diff --git a/api/.gitignore b/api/.gitignore index 6a46fedcb..68797b56a 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -174,4 +174,5 @@ logs*.txt SUBNETS.json ./chalicelib/.configs -README/* \ No newline at end of file +README/* +.local \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index 0d949e25e..20dfe9b86 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,7 +1,6 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base nodejs npm tini ARG envarg # Add Tini diff --git a/api/Dockerfile.alerts b/api/Dockerfile.alerts index c4614b3c1..dbb0c581d 100644 --- a/api/Dockerfile.alerts +++ b/api/Dockerfile.alerts @@ -1,7 +1,6 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base tini ARG envarg ENV APP_NAME=alerts \ diff --git a/api/app.py b/api/app.py index 959f1ef8f..cf00c747b 100644 --- a/api/app.py +++ b/api/app.py @@ -4,6 +4,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from decouple import config from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware from starlette.responses import StreamingResponse from chalicelib.utils import helper @@ -13,8 +14,8 @@ from routers.crons import core_crons from routers.crons import core_dynamic_crons from routers.subs import dashboard, insights, metrics, v1_api -app = FastAPI(root_path="/api") - +app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default="")) +app.add_middleware(GZipMiddleware, minimum_size=1000) @app.middleware('http') async def or_middleware(request: Request, call_next): diff --git a/api/app_alerts.py b/api/app_alerts.py index 57bfcd55d..4e05ab1a8 100644 --- a/api/app_alerts.py +++ b/api/app_alerts.py @@ -6,7 +6,7 @@ from fastapi import FastAPI from chalicelib.core import alerts_processor -app = FastAPI() +app = FastAPI(root_path="/alerts", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default="")) print("============= ALERTS =============") diff --git a/api/chalicelib/core/assist.py b/api/chalicelib/core/assist.py index efbc7b5c6..e382fe348 100644 --- a/api/chalicelib/core/assist.py +++ b/api/chalicelib/core/assist.py @@ -1,6 +1,6 @@ import requests from decouple import config - +from os.path import exists import schemas from chalicelib.core import projects @@ -35,9 +35,10 @@ def get_live_sessions_ws(project_id, body: schemas.LiveSessionsSearchPayloadSche } for f in body.filters: if f.type == schemas.LiveFilterType.metadata: - data["filter"][f.source] = f.value + data["filter"][f.source] = {"values": f.value, "operator": f.operator} + else: - data["filter"][f.type.value] = f.value + data["filter"][f.type.value] = {"values": f.value, "operator": f.operator} return __get_live_sessions_ws(project_id=project_id, data=data) @@ -157,3 +158,11 @@ def autocomplete(project_id, q: str, key: str = None): def get_ice_servers(): return config("iceServers") if config("iceServers", default=None) is not None \ and len(config("iceServers")) > 0 else None + + +def get_raw_mob_by_id(project_id, session_id): + path_to_file = config("FS_DIR") + "/" + str(session_id) + + if exists(path_to_file): + return path_to_file + return None diff --git a/api/chalicelib/core/integration_github.py b/api/chalicelib/core/integration_github.py index a05c946f4..2de8cc518 100644 --- a/api/chalicelib/core/integration_github.py +++ b/api/chalicelib/core/integration_github.py @@ -1,8 +1,9 @@ +import schemas from chalicelib.core import integration_base from chalicelib.core.integration_github_issue import GithubIntegrationIssue from chalicelib.utils import pg_client, helper -PROVIDER = "GITHUB" +PROVIDER = schemas.IntegrationType.github class GitHubIntegration(integration_base.BaseIntegration): diff --git a/api/chalicelib/core/integration_jira_cloud.py b/api/chalicelib/core/integration_jira_cloud.py index 7d8c956cf..469723e4e 100644 --- a/api/chalicelib/core/integration_jira_cloud.py +++ b/api/chalicelib/core/integration_jira_cloud.py @@ -1,8 +1,9 @@ +import schemas from chalicelib.core import integration_base from chalicelib.core.integration_jira_cloud_issue import JIRACloudIntegrationIssue from chalicelib.utils import pg_client, helper -PROVIDER = "JIRA" +PROVIDER = schemas.IntegrationType.jira def obfuscate_string(string): diff --git a/api/chalicelib/core/integrations_global.py b/api/chalicelib/core/integrations_global.py new file mode 100644 index 000000000..5b00a28bd --- /dev/null +++ b/api/chalicelib/core/integrations_global.py @@ -0,0 +1,61 @@ +import schemas +from chalicelib.utils import pg_client + + +def get_global_integrations_status(tenant_id, user_id, project_id): + with pg_client.PostgresClient() as cur: + cur.execute( + cur.mogrify(f"""\ + SELECT EXISTS((SELECT 1 + FROM public.oauth_authentication + WHERE user_id = %(user_id)s + AND provider = 'github')) AS {schemas.IntegrationType.github}, + EXISTS((SELECT 1 + FROM public.jira_cloud + WHERE user_id = %(user_id)s)) AS {schemas.IntegrationType.jira}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='bugsnag')) AS {schemas.IntegrationType.bugsnag}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='cloudwatch')) AS {schemas.IntegrationType.cloudwatch}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='datadog')) AS {schemas.IntegrationType.datadog}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='newrelic')) AS {schemas.IntegrationType.newrelic}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='rollbar')) AS {schemas.IntegrationType.rollbar}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='sentry')) AS {schemas.IntegrationType.sentry}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='stackdriver')) AS {schemas.IntegrationType.stackdriver}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='sumologic')) AS {schemas.IntegrationType.sumologic}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='elasticsearch')) AS {schemas.IntegrationType.elasticsearch}, + EXISTS((SELECT 1 + FROM public.webhooks + WHERE type='slack')) AS {schemas.IntegrationType.slack};""", + {"user_id": user_id, "tenant_id": tenant_id, "project_id": project_id}) + ) + current_integrations = cur.fetchone() + result = [] + for k in current_integrations.keys(): + result.append({"name": k, "integrated": current_integrations[k]}) + return result diff --git a/api/chalicelib/utils/jira_client.py b/api/chalicelib/utils/jira_client.py index b1734660c..4306cfab2 100644 --- a/api/chalicelib/utils/jira_client.py +++ b/api/chalicelib/utils/jira_client.py @@ -18,7 +18,7 @@ class JiraManager: self._config = {"JIRA_PROJECT_ID": project_id, "JIRA_URL": url, "JIRA_USERNAME": username, "JIRA_PASSWORD": password} try: - self._jira = JIRA(url, basic_auth=(username, password), logging=True, max_retries=1) + self._jira = JIRA(url, basic_auth=(username, password), logging=True, max_retries=0, timeout=3) except Exception as e: print("!!! JIRA AUTH ERROR") print(e) diff --git a/api/chalicelib/utils/pg_client.py b/api/chalicelib/utils/pg_client.py index 2abc9f6c7..eda7747f8 100644 --- a/api/chalicelib/utils/pg_client.py +++ b/api/chalicelib/utils/pg_client.py @@ -75,9 +75,11 @@ class PostgresClient: connection = None cursor = None long_query = False + unlimited_query = False def __init__(self, long_query=False, unlimited_query=False): self.long_query = long_query + self.unlimited_query = unlimited_query if unlimited_query: long_config = dict(_PG_CONFIG) long_config["application_name"] += "-UNLIMITED" @@ -85,7 +87,7 @@ class PostgresClient: elif long_query: long_config = dict(_PG_CONFIG) long_config["application_name"] += "-LONG" - long_config["options"] = f"-c statement_timeout={config('pg_long_timeout', cast=int, default=5*60) * 1000}" + long_config["options"] = f"-c statement_timeout={config('pg_long_timeout', cast=int, default=5 * 60) * 1000}" self.connection = psycopg2.connect(**long_config) else: self.connection = postgreSQL_pool.getconn() @@ -99,11 +101,11 @@ class PostgresClient: try: self.connection.commit() self.cursor.close() - if self.long_query: + if self.long_query or self.unlimited_query: self.connection.close() except Exception as error: print("Error while committing/closing PG-connection", error) - if str(error) == "connection already closed": + if str(error) == "connection already closed" and not self.long_query and not self.unlimited_query: print("Recreating the connexion pool") make_pool() else: diff --git a/api/env.default b/api/env.default index aa14fc993..c99e1dc05 100644 --- a/api/env.default +++ b/api/env.default @@ -47,4 +47,5 @@ sessions_region=us-east-1 sourcemaps_bucket=sourcemaps sourcemaps_reader=http://127.0.0.1:9000/sourcemaps stage=default-foss -version_number=1.4.0 \ No newline at end of file +version_number=1.4.0 +FS_DIR=/mnt/efs \ No newline at end of file diff --git a/api/requirements-alerts.txt b/api/requirements-alerts.txt index 81198b0f3..788c58767 100644 --- a/api/requirements-alerts.txt +++ b/api/requirements-alerts.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/api/requirements.txt b/api/requirements.txt index 81198b0f3..788c58767 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/api/routers/core.py b/api/routers/core.py index df98c1c09..2263a30bd 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -2,6 +2,7 @@ from typing import Union, Optional from decouple import config from fastapi import Depends, Body, BackgroundTasks, HTTPException +from fastapi.responses import FileResponse from starlette import status import schemas @@ -12,7 +13,7 @@ from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assig log_tool_cloudwatch, log_tool_sentry, log_tool_sumologic, log_tools, errors, sessions, \ log_tool_newrelic, announcements, log_tool_bugsnag, weekly_report, integration_jira_cloud, integration_github, \ assist, heatmaps, mobile, signup, tenants, errors_favorite_viewed, boarding, notifications, webhook, users, \ - custom_metrics, saved_search + custom_metrics, saved_search, integrations_global from chalicelib.core.collaboration_slack import Slack from chalicelib.utils import email_helper, helper, captcha from chalicelib.utils.TimeUTC import TimeUTC @@ -180,6 +181,14 @@ def session_top_filter_values(projectId: int, context: schemas.CurrentContext = return {'data': sessions_metas.get_top_key_values(projectId)} +@app.get('/{projectId}/integrations', tags=["integrations"]) +def get_integrations_status(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): + data = integrations_global.get_global_integrations_status(tenant_id=context.tenant_id, + user_id=context.user_id, + project_id=projectId) + return {"data": data} + + @app.post('/{projectId}/integrations/{integration}/notify/{integrationId}/{source}/{sourceId}', tags=["integrations"]) @app.put('/{projectId}/integrations/{integration}/notify/{integrationId}/{source}/{sourceId}', tags=["integrations"]) def integration_notify(projectId: int, integration: str, integrationId: int, source: str, sourceId: str, @@ -432,29 +441,47 @@ def get_integration_status(context: schemas.CurrentContext = Depends(OR_context) return {"data": integration.get_obfuscated()} +@app.get('/integrations/jira', tags=["integrations"]) +def get_integration_status_jira(context: schemas.CurrentContext = Depends(OR_context)): + error, integration = integrations_manager.get_integration(tenant_id=context.tenant_id, + user_id=context.user_id, + tool=integration_jira_cloud.PROVIDER) + if error is not None and integration is None: + return error + return {"data": integration.get_obfuscated()} + + +@app.get('/integrations/github', tags=["integrations"]) +def get_integration_status_github(context: schemas.CurrentContext = Depends(OR_context)): + error, integration = integrations_manager.get_integration(tenant_id=context.tenant_id, + user_id=context.user_id, + tool=integration_github.PROVIDER) + if error is not None and integration is None: + return error + return {"data": integration.get_obfuscated()} + + @app.post('/integrations/jira', tags=["integrations"]) @app.put('/integrations/jira', tags=["integrations"]) -def add_edit_jira_cloud(data: schemas.JiraGithubSchema = Body(...), +def add_edit_jira_cloud(data: schemas.JiraSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): error, integration = integrations_manager.get_integration(tool=integration_jira_cloud.PROVIDER, tenant_id=context.tenant_id, user_id=context.user_id) if error is not None and integration is None: return error - data.provider = integration_jira_cloud.PROVIDER return {"data": integration.add_edit(data=data.dict())} @app.post('/integrations/github', tags=["integrations"]) @app.put('/integrations/github', tags=["integrations"]) -def add_edit_github(data: schemas.JiraGithubSchema = Body(...), +def add_edit_github(data: schemas.GithubSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): error, integration = integrations_manager.get_integration(tool=integration_github.PROVIDER, tenant_id=context.tenant_id, user_id=context.user_id) if error is not None: return error - data.provider = integration_github.PROVIDER return {"data": integration.add_edit(data=data.dict())} @@ -887,6 +914,17 @@ def get_live_session(projectId: int, sessionId: str, background_tasks: Backgroun return {'data': data} +@app.get('/{projectId}/unprocessed/{sessionId}', tags=["assist"]) +@app.get('/{projectId}/assist/sessions/{sessionId}/replay', tags=["assist"]) +def get_live_session_replay_file(projectId: int, sessionId: str, + context: schemas.CurrentContext = Depends(OR_context)): + path = assist.get_raw_mob_by_id(project_id=projectId, session_id=sessionId) + if path is None: + return {"errors": ["Replay file not found"]} + + return FileResponse(path=path, media_type="application/octet-stream") + + @app.post('/{projectId}/heatmaps/url', tags=["heatmaps"]) def get_heatmaps_by_url(projectId: int, data: schemas.GetHeatmapPayloadSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): @@ -1171,4 +1209,5 @@ def get_limits(context: schemas.CurrentContext = Depends(OR_context)): @public_app.put('/', tags=["health"]) @public_app.delete('/', tags=["health"]) def health_check(): - return {"data": f"live {config('version_number', default='')}"} + return {"data": {"stage": f"live {config('version_number', default='')}", + "internalCrons": config("LOCAL_CRONS", default=False, cast=bool)}} diff --git a/api/routers/core_dynamic.py b/api/routers/core_dynamic.py index 594715bb6..bddb0ae4d 100644 --- a/api/routers/core_dynamic.py +++ b/api/routers/core_dynamic.py @@ -87,18 +87,6 @@ def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = B changes={"name": data.name, "endpoint": data.url})} -# this endpoint supports both jira & github based on `provider` attribute -@app.post('/integrations/issues', tags=["integrations"]) -def add_edit_jira_cloud_github(data: schemas.JiraGithubSchema, - context: schemas.CurrentContext = Depends(OR_context)): - provider = data.provider.upper() - error, integration = integrations_manager.get_integration(tool=provider, tenant_id=context.tenant_id, - user_id=context.user_id) - if error is not None: - return error - return {"data": integration.add_edit(data=data.dict())} - - @app.post('/client/members', tags=["client"]) @app.put('/client/members', tags=["client"]) def add_member(background_tasks: BackgroundTasks, data: schemas.CreateMemberSchema = Body(...), diff --git a/api/schemas.py b/api/schemas.py index f0f20e657..2289c03cb 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -100,10 +100,12 @@ class NotificationsViewSchema(BaseModel): endTimestamp: Optional[int] = Field(default=None) -class JiraGithubSchema(BaseModel): - provider: str = Field(...) - username: str = Field(...) +class GithubSchema(BaseModel): token: str = Field(...) + + +class JiraSchema(GithubSchema): + username: str = Field(...) url: HttpUrl = Field(...) @validator('url') @@ -1024,13 +1026,15 @@ class LiveFilterType(str, Enum): user_UUID = "USERUUID" tracker_version = "TRACKERVERSION" user_browser_version = "USERBROWSERVERSION" - user_device_type = "USERDEVICETYPE", + user_device_type = "USERDEVICETYPE" class LiveSessionSearchFilterSchema(BaseModel): value: Union[List[str], str] = Field(...) type: LiveFilterType = Field(...) source: Optional[str] = Field(None) + operator: Literal[SearchEventOperator._is.value, + SearchEventOperator._contains.value] = Field(SearchEventOperator._contains.value) @root_validator def validator(cls, values): @@ -1066,3 +1070,18 @@ class LiveSessionsSearchPayloadSchema(_PaginatedSchema): class Config: alias_generator = attribute_to_camel_case + + +class IntegrationType(str, Enum): + github = "GITHUB" + jira = "JIRA" + slack = "SLACK" + sentry = "SENTRY" + bugsnag = "BUGSNAG" + rollbar = "ROLLBAR" + elasticsearch = "ELASTICSEARCH" + datadog = "DATADOG" + sumologic = "SUMOLOGIC" + stackdriver = "STACKDRIVER" + cloudwatch = "CLOUDWATCH" + newrelic = "NEWRELIC" diff --git a/backend/Dockerfile b/backend/Dockerfile index 4f060587d..c2375d800 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -19,7 +19,6 @@ RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o service -tags musl openrep FROM alpine AS entrypoint -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache ca-certificates RUN adduser -u 1001 openreplay -D @@ -28,7 +27,7 @@ ENV TZ=UTC \ FS_DIR=/mnt/efs \ MAXMINDDB_FILE=/home/openreplay/geoip.mmdb \ UAPARSER_FILE=/home/openreplay/regexes.yaml \ - HTTP_PORT=80 \ + HTTP_PORT=8080 \ KAFKA_USE_SSL=true \ KAFKA_MAX_POLL_INTERVAL_MS=400000 \ REDIS_STREAMS_MAX_LEN=10000 \ @@ -46,11 +45,12 @@ ENV TZ=UTC \ AWS_REGION_WEB=eu-central-1 \ AWS_REGION_IOS=eu-west-1 \ AWS_REGION_ASSETS=eu-central-1 \ + AWS_SKIP_SSL_VALIDATION=false \ CACHE_ASSETS=true \ ASSETS_SIZE_LIMIT=6291456 \ ASSETS_HEADERS="{ \"Cookie\": \"ABv=3;\" }" \ FS_CLEAN_HRS=72 \ - FILE_SPLIT_SIZE=500000 \ + FILE_SPLIT_SIZE=1000000 \ LOG_QUEUE_STATS_INTERVAL_SEC=60 \ DB_BATCH_QUEUE_LIMIT=20 \ DB_BATCH_SIZE_LIMIT=10000000 \ diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index 2e962cb1b..1712b8a3f 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -122,11 +122,18 @@ func main() { os.Exit(0) case <-commitTick: // Send collected batches to db + start := time.Now() pg.CommitBatches() + pgDur := time.Now().Sub(start).Milliseconds() + + start = time.Now() if err := saver.CommitStats(); err != nil { log.Printf("Error on stats commit: %v", err) } - // TODO?: separate stats & regular messages + chDur := time.Now().Sub(start).Milliseconds() + log.Printf("commit duration(ms), pg: %d, ch: %d", pgDur, chDur) + + // TODO: use commit worker to save time each tick if err := consumer.Commit(); err != nil { log.Printf("Error on consumer commit: %v", err) } @@ -134,7 +141,7 @@ func main() { // Handle new message from queue err := consumer.ConsumeNext() if err != nil { - log.Fatalf("Error on consumption: %v", err) // TODO: is always fatal? + log.Fatalf("Error on consumption: %v", err) } } } diff --git a/backend/cmd/ender/main.go b/backend/cmd/ender/main.go index c0613fca0..1fd2f4e64 100644 --- a/backend/cmd/ender/main.go +++ b/backend/cmd/ender/main.go @@ -82,10 +82,20 @@ func main() { // Find ended sessions and send notification to other services sessions.HandleEndedSessions(func(sessionID uint64, timestamp int64) bool { msg := &messages.SessionEnd{Timestamp: uint64(timestamp)} - if err := pg.InsertSessionEnd(sessionID, msg.Timestamp); err != nil { + currDuration, err := pg.GetSessionDuration(sessionID) + if err != nil { + log.Printf("getSessionDuration failed, sessID: %d, err: %s", sessionID, err) + } + newDuration, err := pg.InsertSessionEnd(sessionID, msg.Timestamp) + if err != nil { log.Printf("can't save sessionEnd to database, sessID: %d, err: %s", sessionID, err) return false } + if currDuration == newDuration { + log.Printf("sessionEnd duplicate, sessID: %d, prevDur: %d, newDur: %d", sessionID, + currDuration, newDuration) + return true + } if err := producer.Produce(cfg.TopicRawWeb, sessionID, messages.Encode(msg)); err != nil { log.Printf("can't send sessionEnd to topic: %s; sessID: %d", err, sessionID) return false diff --git a/backend/go.mod b/backend/go.mod index a15e23196..caaf1bf83 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,12 +4,12 @@ go 1.18 require ( cloud.google.com/go/logging v1.4.2 - github.com/ClickHouse/clickhouse-go v1.5.4 + github.com/ClickHouse/clickhouse-go/v2 v2.2.0 github.com/aws/aws-sdk-go v1.35.23 github.com/btcsuite/btcutil v1.0.2 github.com/elastic/go-elasticsearch/v7 v7.13.1 github.com/go-redis/redis v6.15.9+incompatible - github.com/google/uuid v1.1.2 + github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/jackc/pgconn v1.6.0 github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 @@ -36,7 +36,6 @@ require ( cloud.google.com/go/storage v1.14.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 // indirect github.com/confluentinc/confluent-kafka-go v1.9.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -50,15 +49,19 @@ require ( github.com/jackc/pgproto3/v2 v2.0.2 // indirect github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8 // indirect github.com/jackc/pgtype v1.3.0 // indirect - github.com/jackc/puddle v1.1.0 // indirect + github.com/jackc/puddle v1.2.2-0.20220404125616-4e959849469a // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/klauspost/compress v1.11.9 // indirect + github.com/klauspost/compress v1.15.7 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/paulmach/orb v0.7.1 // indirect + github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/stretchr/testify v1.8.0 // indirect go.opencensus.io v0.23.0 // indirect go.opentelemetry.io/otel/sdk v1.7.0 // indirect go.opentelemetry.io/otel/trace v1.7.0 // indirect @@ -73,5 +76,4 @@ require ( google.golang.org/grpc v1.46.2 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 433f2b895..6b76d1278 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -61,9 +61,11 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/clickhouse-go v1.5.4 h1:cKjXeYLNWVJIx2J1K6H2CqyRmfwVJVY1OV1coaaFcI0= github.com/ClickHouse/clickhouse-go v1.5.4/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/ClickHouse/clickhouse-go/v2 v2.2.0 h1:dj00TDKY+xwuTJdbpspCSmTLFyWzRJerTHwaBxut1C0= +github.com/ClickHouse/clickhouse-go/v2 v2.2.0/go.mod h1:8f2XZUi7XoeU+uPIytSi1cvx8fmJxi7vIgqpvYTF1+o= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -79,7 +81,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk= github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= @@ -100,7 +101,6 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -151,6 +151,8 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -158,6 +160,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -230,8 +233,9 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -240,8 +244,10 @@ github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/Oth github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -290,8 +296,9 @@ github.com/jackc/pgx/v4 v4.6.0 h1:Fh0O9GdlG4gYpjpwOqjdEodJUQM9jzN3Hdv7PN0xmm0= github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.0 h1:musOWczZC/rSbqut475Vfcczg7jJsdUQf0D6oKPLgNU= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.2-0.20220404125616-4e959849469a h1:oH7y/b+q2BEerCnARr/HZc1NxOYbKSJor4MqQXlhh+s= +github.com/jackc/puddle v1.2.2-0.20220404125616-4e959849469a/go.mod h1:ZQuO1Un86Xpe1ShKl08ERTzYhzWq+OvrvotbpeE3XO0= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -308,10 +315,11 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/klauspost/compress v1.11.9 h1:5OCMOdde1TCT2sookEuVeEZzA8bmRSFV3AwPDZAG8AA= -github.com/klauspost/compress v1.11.9/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.7 h1:7cgTQxJCU/vy+oP/E3B9RGbQTgbiVzIJWIKOLoAsPok= +github.com/klauspost/compress v1.15.7/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -339,6 +347,7 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mkevac/debugcharts v0.0.0-20191222103121-ae1c48aa8615/go.mod h1:Ad7oeElCZqA1Ufj0U9/liOF4BtVepxRcTvr2ey7zTvM= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -353,8 +362,12 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/oschwald/maxminddb-golang v1.7.0 h1:JmU4Q1WBv5Q+2KZy5xJI+98aUwTIrPPxZUkd5Cwr8Zc= github.com/oschwald/maxminddb-golang v1.7.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis= -github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/paulmach/orb v0.7.1 h1:Zha++Z5OX/l168sqHK3k4z18LDvr+YAO/VjK0ReQ9rU= +github.com/paulmach/orb v0.7.1/go.mod h1:FWRlTgl88VI1RBx/MkrwWDRhQ96ctqMCh8boXhmqB/A= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -393,8 +406,12 @@ github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThC github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sethvargo/go-envconfig v0.7.0 h1:P/ljQXSRjgAgsnIripHs53Jg/uNVXu2FYQ9yLSDappA= github.com/sethvargo/go-envconfig v0.7.0/go.mod h1:00S1FAhRUuTNJazWBWcJGvEHOM+NO6DhoRMAOX7FY5o= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= +github.com/shirou/gopsutil v2.19.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -403,14 +420,19 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= +github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/ua-parser/uap-go v0.0.0-20200325213135-e1c09f13e2fe h1:aj/vX5epIlQQBEocKoM9nSAiNpakdQzElc8SaRFPu+I= @@ -420,6 +442,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -594,8 +617,10 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220220014-0732a990476f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -649,6 +674,7 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220429233432-b5fbb4746d32/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -706,6 +732,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -714,6 +741,7 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -927,8 +955,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go index abe8089be..8d79468db 100644 --- a/backend/internal/storage/storage.go +++ b/backend/internal/storage/storage.go @@ -21,6 +21,7 @@ type Storage struct { startBytes []byte totalSessions syncfloat64.Counter sessionSize syncfloat64.Histogram + readingTime syncfloat64.Histogram archivingTime syncfloat64.Histogram } @@ -40,6 +41,10 @@ func New(cfg *config.Config, s3 *storage.S3, metrics *monitoring.Metrics) (*Stor if err != nil { log.Printf("can't create session_size metric: %s", err) } + readingTime, err := metrics.RegisterHistogram("reading_duration") + if err != nil { + log.Printf("can't create reading_duration metric: %s", err) + } archivingTime, err := metrics.RegisterHistogram("archiving_duration") if err != nil { log.Printf("can't create archiving_duration metric: %s", err) @@ -50,16 +55,17 @@ func New(cfg *config.Config, s3 *storage.S3, metrics *monitoring.Metrics) (*Stor startBytes: make([]byte, cfg.FileSplitSize), totalSessions: totalSessions, sessionSize: sessionSize, + readingTime: readingTime, archivingTime: archivingTime, }, nil } func (s *Storage) UploadKey(key string, retryCount int) error { - start := time.Now() if retryCount <= 0 { return nil } + start := time.Now() file, err := os.Open(s.cfg.FSDir + "/" + key) if err != nil { sessID, _ := strconv.ParseUint(key, 10, 64) @@ -84,6 +90,9 @@ func (s *Storage) UploadKey(key string, retryCount int) error { }) return nil } + s.readingTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds())) + + start = time.Now() startReader := bytes.NewBuffer(s.startBytes[:nRead]) if err := s.s3.Upload(s.gzipFile(startReader), key, "application/octet-stream", true); err != nil { log.Fatalf("Storage: start upload failed. %v\n", err) @@ -93,6 +102,7 @@ func (s *Storage) UploadKey(key string, retryCount int) error { log.Fatalf("Storage: end upload failed. %v\n", err) } } + s.archivingTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds())) // Save metrics var fileSize float64 = 0 @@ -103,7 +113,7 @@ func (s *Storage) UploadKey(key string, retryCount int) error { fileSize = float64(fileInfo.Size()) } ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*200) - s.archivingTime.Record(ctx, float64(time.Now().Sub(start).Milliseconds())) + s.sessionSize.Record(ctx, fileSize) s.totalSessions.Add(ctx, 1) return nil diff --git a/backend/pkg/db/cache/messages-common.go b/backend/pkg/db/cache/messages-common.go index cebdaf5e7..41cdb1895 100644 --- a/backend/pkg/db/cache/messages-common.go +++ b/backend/pkg/db/cache/messages-common.go @@ -7,12 +7,8 @@ import ( // . "openreplay/backend/pkg/db/types" ) -func (c *PGCache) InsertSessionEnd(sessionID uint64, timestamp uint64) error { - _, err := c.Conn.InsertSessionEnd(sessionID, timestamp) - if err != nil { - return err - } - return nil +func (c *PGCache) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, error) { + return c.Conn.InsertSessionEnd(sessionID, timestamp) } func (c *PGCache) HandleSessionEnd(sessionID uint64) error { diff --git a/backend/pkg/db/cache/messages-ios.go b/backend/pkg/db/cache/messages-ios.go index 4195976c3..e0463c431 100644 --- a/backend/pkg/db/cache/messages-ios.go +++ b/backend/pkg/db/cache/messages-ios.go @@ -32,7 +32,8 @@ func (c *PGCache) InsertIOSSessionStart(sessionID uint64, s *IOSSessionStart) er } func (c *PGCache) InsertIOSSessionEnd(sessionID uint64, e *IOSSessionEnd) error { - return c.InsertSessionEnd(sessionID, e.Timestamp) + _, err := c.InsertSessionEnd(sessionID, e.Timestamp) + return err } func (c *PGCache) InsertIOSScreenEnter(sessionID uint64, screenEnter *IOSScreenEnter) error { @@ -84,13 +85,5 @@ func (c *PGCache) InsertIOSCrash(sessionID uint64, crash *IOSCrash) error { } func (c *PGCache) InsertIOSIssueEvent(sessionID uint64, issueEvent *IOSIssueEvent) error { - // session, err := c.GetSession(sessionID) - // if err != nil { - // return err - // } - // TODO: unite IssueEvent message for the all platforms - // if err := c.Conn.InsertIssueEvent(sessionID, session.ProjectID, issueEvent); err != nil { - // return err - // } return nil } diff --git a/backend/pkg/db/cache/messages-web.go b/backend/pkg/db/cache/messages-web.go index 7da7006af..0a864e6d3 100644 --- a/backend/pkg/db/cache/messages-web.go +++ b/backend/pkg/db/cache/messages-web.go @@ -63,7 +63,8 @@ func (c *PGCache) HandleWebSessionStart(sessionID uint64, s *SessionStart) error } func (c *PGCache) InsertWebSessionEnd(sessionID uint64, e *SessionEnd) error { - return c.InsertSessionEnd(sessionID, e.Timestamp) + _, err := c.InsertSessionEnd(sessionID, e.Timestamp) + return err } func (c *PGCache) HandleWebSessionEnd(sessionID uint64, e *SessionEnd) error { @@ -91,7 +92,7 @@ func (c *PGCache) InsertWebFetchEvent(sessionID uint64, e *FetchEvent) error { if err != nil { return err } - return c.Conn.InsertWebFetchEvent(sessionID, project.SaveRequestPayloads, e) + return c.Conn.InsertWebFetchEvent(sessionID, session.ProjectID, project.SaveRequestPayloads, e) } func (c *PGCache) InsertWebGraphQLEvent(sessionID uint64, e *GraphQLEvent) error { @@ -103,5 +104,53 @@ func (c *PGCache) InsertWebGraphQLEvent(sessionID uint64, e *GraphQLEvent) error if err != nil { return err } - return c.Conn.InsertWebGraphQLEvent(sessionID, project.SaveRequestPayloads, e) + return c.Conn.InsertWebGraphQLEvent(sessionID, session.ProjectID, project.SaveRequestPayloads, e) +} + +func (c *PGCache) InsertWebCustomEvent(sessionID uint64, e *CustomEvent) error { + session, err := c.GetSession(sessionID) + if err != nil { + return err + } + return c.Conn.InsertWebCustomEvent(sessionID, session.ProjectID, e) +} + +func (c *PGCache) InsertWebUserID(sessionID uint64, userID *UserID) error { + session, err := c.GetSession(sessionID) + if err != nil { + return err + } + return c.Conn.InsertWebUserID(sessionID, session.ProjectID, userID) +} + +func (c *PGCache) InsertWebUserAnonymousID(sessionID uint64, userAnonymousID *UserAnonymousID) error { + session, err := c.GetSession(sessionID) + if err != nil { + return err + } + return c.Conn.InsertWebUserAnonymousID(sessionID, session.ProjectID, userAnonymousID) +} + +func (c *PGCache) InsertWebPageEvent(sessionID uint64, e *PageEvent) error { + session, err := c.GetSession(sessionID) + if err != nil { + return err + } + return c.Conn.InsertWebPageEvent(sessionID, session.ProjectID, e) +} + +func (c *PGCache) InsertWebClickEvent(sessionID uint64, e *ClickEvent) error { + session, err := c.GetSession(sessionID) + if err != nil { + return err + } + return c.Conn.InsertWebClickEvent(sessionID, session.ProjectID, e) +} + +func (c *PGCache) InsertWebInputEvent(sessionID uint64, e *InputEvent) error { + session, err := c.GetSession(sessionID) + if err != nil { + return err + } + return c.Conn.InsertWebInputEvent(sessionID, session.ProjectID, e) } diff --git a/backend/pkg/db/cache/pg-cache.go b/backend/pkg/db/cache/pg-cache.go index 6422209d4..ca31bcd82 100644 --- a/backend/pkg/db/cache/pg-cache.go +++ b/backend/pkg/db/cache/pg-cache.go @@ -29,10 +29,9 @@ type PGCache struct { // TODO: create conn automatically func NewPGCache(pgConn *postgres.Conn, projectExpirationTimeoutMs int64) *PGCache { return &PGCache{ - Conn: pgConn, - sessions: make(map[uint64]*Session), - projects: make(map[uint32]*ProjectMeta), - //projectsByKeys: make(map[string]*ProjectMeta), + Conn: pgConn, + sessions: make(map[uint64]*Session), + projects: make(map[uint32]*ProjectMeta), projectExpirationTimeout: time.Duration(1000 * projectExpirationTimeoutMs), } } diff --git a/backend/pkg/db/postgres/bulk.go b/backend/pkg/db/postgres/bulk.go new file mode 100644 index 000000000..16f59efcd --- /dev/null +++ b/backend/pkg/db/postgres/bulk.go @@ -0,0 +1,93 @@ +package postgres + +import ( + "bytes" + "errors" + "fmt" +) + +const ( + insertPrefix = `INSERT INTO ` + insertValues = ` VALUES ` + insertSuffix = ` ON CONFLICT DO NOTHING;` +) + +type Bulk interface { + Append(args ...interface{}) error + Send() error +} + +type bulkImpl struct { + conn Pool + table string + columns string + template string + setSize int + sizeLimit int + values []interface{} +} + +func (b *bulkImpl) Append(args ...interface{}) error { + if len(args) != b.setSize { + return fmt.Errorf("wrong number of arguments, waited: %d, got: %d", b.setSize, len(args)) + } + b.values = append(b.values, args...) + if len(b.values)/b.setSize >= b.sizeLimit { + return b.send() + } + return nil +} + +func (b *bulkImpl) Send() error { + if len(b.values) == 0 { + return nil + } + return b.send() +} + +func (b *bulkImpl) send() error { + request := bytes.NewBufferString(insertPrefix + b.table + b.columns + insertValues) + args := make([]interface{}, b.setSize) + for i := 0; i < len(b.values)/b.setSize; i++ { + for j := 0; j < b.setSize; j++ { + args[j] = i*b.setSize + j + 1 + } + if i > 0 { + request.WriteByte(',') + } + request.WriteString(fmt.Sprintf(b.template, args...)) + } + request.WriteString(insertSuffix) + err := b.conn.Exec(request.String(), b.values...) + b.values = make([]interface{}, 0, b.setSize*b.sizeLimit) + if err != nil { + return fmt.Errorf("send bulk err: %s", err) + } + return nil +} + +func NewBulk(conn Pool, table, columns, template string, setSize, sizeLimit int) (Bulk, error) { + switch { + case conn == nil: + return nil, errors.New("db conn is empty") + case table == "": + return nil, errors.New("table is empty") + case columns == "": + return nil, errors.New("columns is empty") + case template == "": + return nil, errors.New("template is empty") + case setSize <= 0: + return nil, errors.New("set size is wrong") + case sizeLimit <= 0: + return nil, errors.New("size limit is wrong") + } + return &bulkImpl{ + conn: conn, + table: table, + columns: columns, + template: template, + setSize: setSize, + sizeLimit: sizeLimit, + values: make([]interface{}, 0, setSize*sizeLimit), + }, nil +} diff --git a/backend/pkg/db/postgres/connector.go b/backend/pkg/db/postgres/connector.go index 4a85029fd..1cc537982 100644 --- a/backend/pkg/db/postgres/connector.go +++ b/backend/pkg/db/postgres/connector.go @@ -13,26 +13,30 @@ import ( "github.com/jackc/pgx/v4/pgxpool" ) -func getTimeoutContext() context.Context { - ctx, _ := context.WithTimeout(context.Background(), time.Duration(time.Second*30)) - return ctx -} - type batchItem struct { query string arguments []interface{} } +// Conn contains batches, bulks and cache for all sessions type Conn struct { - c *pgxpool.Pool // TODO: conditional usage of Pool/Conn (use interface?) - batches map[uint64]*pgx.Batch - batchSizes map[uint64]int - rawBatches map[uint64][]*batchItem - batchQueueLimit int - batchSizeLimit int - batchSizeBytes syncfloat64.Histogram - batchSizeLines syncfloat64.Histogram - sqlRequestTime syncfloat64.Histogram + c Pool + batches map[uint64]*pgx.Batch + batchSizes map[uint64]int + rawBatches map[uint64][]*batchItem + autocompletes Bulk + requests Bulk + customEvents Bulk + webPageEvents Bulk + webInputEvents Bulk + webGraphQLEvents Bulk + sessionUpdates map[uint64]*sessionUpdates + batchQueueLimit int + batchSizeLimit int + batchSizeBytes syncfloat64.Histogram + batchSizeLines syncfloat64.Histogram + sqlRequestTime syncfloat64.Histogram + sqlRequestCounter syncfloat64.Counter } func NewConn(url string, queueLimit, sizeLimit int, metrics *monitoring.Metrics) *Conn { @@ -45,14 +49,19 @@ func NewConn(url string, queueLimit, sizeLimit int, metrics *monitoring.Metrics) log.Fatalln("pgxpool.Connect Error") } conn := &Conn{ - c: c, batches: make(map[uint64]*pgx.Batch), batchSizes: make(map[uint64]int), rawBatches: make(map[uint64][]*batchItem), + sessionUpdates: make(map[uint64]*sessionUpdates), batchQueueLimit: queueLimit, batchSizeLimit: sizeLimit, } conn.initMetrics(metrics) + conn.c, err = NewPool(c, conn.sqlRequestTime, conn.sqlRequestCounter) + if err != nil { + log.Fatalf("can't create new pool wrapper: %s", err) + } + conn.initBulks() return conn } @@ -75,6 +84,74 @@ func (conn *Conn) initMetrics(metrics *monitoring.Metrics) { if err != nil { log.Printf("can't create sqlRequestTime metric: %s", err) } + conn.sqlRequestCounter, err = metrics.RegisterCounter("sql_request_number") + if err != nil { + log.Printf("can't create sqlRequestNumber metric: %s", err) + } +} + +func (conn *Conn) initBulks() { + var err error + conn.autocompletes, err = NewBulk(conn.c, + "autocomplete", + "(value, type, project_id)", + "($%d, $%d, $%d)", + 3, 100) + if err != nil { + log.Fatalf("can't create autocomplete bulk") + } + conn.requests, err = NewBulk(conn.c, + "events_common.requests", + "(session_id, timestamp, seq_index, url, duration, success)", + "($%d, $%d, $%d, left($%d, 2700), $%d, $%d)", + 6, 100) + if err != nil { + log.Fatalf("can't create requests bulk") + } + conn.customEvents, err = NewBulk(conn.c, + "events_common.customs", + "(session_id, timestamp, seq_index, name, payload)", + "($%d, $%d, $%d, left($%d, 2700), $%d)", + 5, 100) + if err != nil { + log.Fatalf("can't create customEvents bulk") + } + conn.webPageEvents, err = NewBulk(conn.c, + "events.pages", + "(session_id, message_id, timestamp, referrer, base_referrer, host, path, query, dom_content_loaded_time, "+ + "load_time, response_end, first_paint_time, first_contentful_paint_time, speed_index, visually_complete, "+ + "time_to_interactive, response_time, dom_building_time)", + "($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0),"+ + " NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0))", + 18, 100) + if err != nil { + log.Fatalf("can't create webPageEvents bulk") + } + conn.webInputEvents, err = NewBulk(conn.c, + "events.inputs", + "(session_id, message_id, timestamp, value, label)", + "($%d, $%d, $%d, $%d, NULLIF($%d,''))", + 5, 100) + if err != nil { + log.Fatalf("can't create webPageEvents bulk") + } + conn.webGraphQLEvents, err = NewBulk(conn.c, + "events.graphql", + "(session_id, timestamp, message_id, name, request_body, response_body)", + "($%d, $%d, $%d, left($%d, 2700), $%d, $%d)", + 6, 100) + if err != nil { + log.Fatalf("can't create webPageEvents bulk") + } +} + +func (conn *Conn) insertAutocompleteValue(sessionID uint64, projectID uint32, tp string, value string) { + if len(value) == 0 { + return + } + if err := conn.autocompletes.Append(value, tp, projectID); err != nil { + log.Printf("autocomplete bulk err: %s", err) + } } func (conn *Conn) batchQueue(sessionID uint64, sql string, args ...interface{}) { @@ -85,6 +162,10 @@ func (conn *Conn) batchQueue(sessionID uint64, sql string, args ...interface{}) batch = conn.batches[sessionID] } batch.Queue(sql, args...) + conn.rawBatch(sessionID, sql, args...) +} + +func (conn *Conn) rawBatch(sessionID uint64, sql string, args ...interface{}) { // Temp raw batch store raw := conn.rawBatches[sessionID] raw = append(raw, &batchItem{ @@ -94,13 +175,54 @@ func (conn *Conn) batchQueue(sessionID uint64, sql string, args ...interface{}) conn.rawBatches[sessionID] = raw } +func (conn *Conn) updateSessionEvents(sessionID uint64, events, pages int) { + if _, ok := conn.sessionUpdates[sessionID]; !ok { + conn.sessionUpdates[sessionID] = NewSessionUpdates(sessionID) + } + conn.sessionUpdates[sessionID].add(pages, events) +} + +func (conn *Conn) sendBulks() { + if err := conn.autocompletes.Send(); err != nil { + log.Printf("autocomplete bulk send err: %s", err) + } + if err := conn.requests.Send(); err != nil { + log.Printf("requests bulk send err: %s", err) + } + if err := conn.customEvents.Send(); err != nil { + log.Printf("customEvents bulk send err: %s", err) + } + if err := conn.webPageEvents.Send(); err != nil { + log.Printf("webPageEvents bulk send err: %s", err) + } + if err := conn.webInputEvents.Send(); err != nil { + log.Printf("webInputEvents bulk send err: %s", err) + } + if err := conn.webGraphQLEvents.Send(); err != nil { + log.Printf("webGraphQLEvents bulk send err: %s", err) + } +} + func (conn *Conn) CommitBatches() { + conn.sendBulks() for sessID, b := range conn.batches { + // Append session update sql request to the end of batch + if update, ok := conn.sessionUpdates[sessID]; ok { + sql, args := update.request() + if sql != "" { + conn.batchQueue(sessID, sql, args...) + b, _ = conn.batches[sessID] + } + } // Record batch size in bytes and number of lines conn.batchSizeBytes.Record(context.Background(), float64(conn.batchSizes[sessID])) conn.batchSizeLines.Record(context.Background(), float64(b.Len())) + + start := time.Now() + isFailed := false + // Send batch to db and execute - br := conn.c.SendBatch(getTimeoutContext(), b) + br := conn.c.SendBatch(b) l := b.Len() for i := 0; i < l; i++ { if ct, err := br.Exec(); err != nil { @@ -108,13 +230,33 @@ func (conn *Conn) CommitBatches() { failedSql := conn.rawBatches[sessID][i] query := strings.ReplaceAll(failedSql.query, "\n", " ") log.Println("failed sql req:", query, failedSql.arguments) + isFailed = true } } br.Close() // returns err + conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", "batch"), attribute.Bool("failed", isFailed)) + conn.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", "batch"), attribute.Bool("failed", isFailed)) + if !isFailed { + delete(conn.sessionUpdates, sessID) + } } conn.batches = make(map[uint64]*pgx.Batch) conn.batchSizes = make(map[uint64]int) conn.rawBatches = make(map[uint64][]*batchItem) + + // Session updates + for sessID, su := range conn.sessionUpdates { + sql, args := su.request() + if sql == "" { + continue + } + if err := conn.c.Exec(sql, args...); err != nil { + log.Printf("failed session update, sessID: %d, err: %s", sessID, err) + } + } + conn.sessionUpdates = make(map[uint64]*sessionUpdates) } func (conn *Conn) updateBatchSize(sessionID uint64, reqSize int) { @@ -131,11 +273,23 @@ func (conn *Conn) commitBatch(sessionID uint64) { log.Printf("can't find batch for session: %d", sessionID) return } + // Append session update sql request to the end of batch + if update, ok := conn.sessionUpdates[sessionID]; ok { + sql, args := update.request() + if sql != "" { + conn.batchQueue(sessionID, sql, args...) + b, _ = conn.batches[sessionID] + } + } // Record batch size in bytes and number of lines conn.batchSizeBytes.Record(context.Background(), float64(conn.batchSizes[sessionID])) conn.batchSizeLines.Record(context.Background(), float64(b.Len())) + + start := time.Now() + isFailed := false + // Send batch to db and execute - br := conn.c.SendBatch(getTimeoutContext(), b) + br := conn.c.SendBatch(b) l := b.Len() for i := 0; i < l; i++ { if ct, err := br.Exec(); err != nil { @@ -143,74 +297,19 @@ func (conn *Conn) commitBatch(sessionID uint64) { failedSql := conn.rawBatches[sessionID][i] query := strings.ReplaceAll(failedSql.query, "\n", " ") log.Println("failed sql req:", query, failedSql.arguments) + isFailed = true } } br.Close() + conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", "batch"), attribute.Bool("failed", isFailed)) + conn.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", "batch"), attribute.Bool("failed", isFailed)) + // Clean batch info delete(conn.batches, sessionID) delete(conn.batchSizes, sessionID) delete(conn.rawBatches, sessionID) -} - -func (conn *Conn) query(sql string, args ...interface{}) (pgx.Rows, error) { - start := time.Now() - res, err := conn.c.Query(getTimeoutContext(), sql, args...) - conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), attribute.String("method", methodName(sql))) - return res, err -} - -func (conn *Conn) queryRow(sql string, args ...interface{}) pgx.Row { - start := time.Now() - res := conn.c.QueryRow(getTimeoutContext(), sql, args...) - conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), attribute.String("method", methodName(sql))) - return res -} - -func (conn *Conn) exec(sql string, args ...interface{}) error { - start := time.Now() - _, err := conn.c.Exec(getTimeoutContext(), sql, args...) - conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), attribute.String("method", methodName(sql))) - return err -} - -type _Tx struct { - pgx.Tx - sqlRequestTime syncfloat64.Histogram -} - -func (conn *Conn) begin() (_Tx, error) { - start := time.Now() - tx, err := conn.c.Begin(context.Background()) - conn.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), attribute.String("method", "begin")) - return _Tx{tx, conn.sqlRequestTime}, err -} - -func (tx _Tx) exec(sql string, args ...interface{}) error { - start := time.Now() - _, err := tx.Exec(context.Background(), sql, args...) - tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), attribute.String("method", methodName(sql))) - return err -} - -func (tx _Tx) rollback() error { - start := time.Now() - err := tx.Rollback(context.Background()) - tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), attribute.String("method", "rollback")) - return err -} - -func (tx _Tx) commit() error { - start := time.Now() - err := tx.Commit(context.Background()) - tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), attribute.String("method", "commit")) - return err -} - -func methodName(sql string) string { - method := "unknown" - if parts := strings.Split(sql, ""); len(parts) > 0 { - method = parts[0] - } - return strings.ToLower(method) + delete(conn.sessionUpdates, sessionID) } diff --git a/backend/pkg/db/postgres/integration.go b/backend/pkg/db/postgres/integration.go index e44bd726e..1556006c1 100644 --- a/backend/pkg/db/postgres/integration.go +++ b/backend/pkg/db/postgres/integration.go @@ -15,7 +15,7 @@ type Integration struct { } func (conn *Conn) IterateIntegrationsOrdered(iter func(integration *Integration, err error)) error { - rows, err := conn.query(` + rows, err := conn.c.Query(` SELECT project_id, provider, options, request_data FROM integrations `) @@ -40,7 +40,7 @@ func (conn *Conn) IterateIntegrationsOrdered(iter func(integration *Integration, } func (conn *Conn) UpdateIntegrationRequestData(i *Integration) error { - return conn.exec(` + return conn.c.Exec(` UPDATE integrations SET request_data = $1 WHERE project_id=$2 AND provider=$3`, diff --git a/backend/pkg/db/postgres/messages-common.go b/backend/pkg/db/postgres/messages-common.go index a68d2c814..2925acde3 100644 --- a/backend/pkg/db/postgres/messages-common.go +++ b/backend/pkg/db/postgres/messages-common.go @@ -18,31 +18,8 @@ func getAutocompleteType(baseType string, platform string) string { } -func (conn *Conn) insertAutocompleteValue(sessionID uint64, tp string, value string) { - if len(value) == 0 { - return - } - sqlRequest := ` - INSERT INTO autocomplete ( - value, - type, - project_id - ) (SELECT - $1, $2, project_id - FROM sessions - WHERE session_id = $3 - ) ON CONFLICT DO NOTHING` - if err := conn.exec(sqlRequest, value, tp, sessionID); err != nil { - log.Printf("can't insert autocomplete: %s", err) - } - //conn.batchQueue(sessionID, sqlRequest, value, tp, sessionID) - - // Record approximate message size - //conn.updateBatchSize(sessionID, len(sqlRequest)+len(value)+len(tp)+8) -} - func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { - return conn.exec(` + return conn.c.Exec(` INSERT INTO sessions ( session_id, project_id, start_ts, user_uuid, user_device, user_device_type, user_country, @@ -74,18 +51,26 @@ func (conn *Conn) InsertSessionStart(sessionID uint64, s *types.Session) error { } func (conn *Conn) HandleSessionStart(sessionID uint64, s *types.Session) error { - conn.insertAutocompleteValue(sessionID, getAutocompleteType("USEROS", s.Platform), s.UserOS) - conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERDEVICE", s.Platform), s.UserDevice) - conn.insertAutocompleteValue(sessionID, getAutocompleteType("USERCOUNTRY", s.Platform), s.UserCountry) - conn.insertAutocompleteValue(sessionID, getAutocompleteType("REVID", s.Platform), s.RevID) + conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USEROS", s.Platform), s.UserOS) + conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USERDEVICE", s.Platform), s.UserDevice) + conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("USERCOUNTRY", s.Platform), s.UserCountry) + conn.insertAutocompleteValue(sessionID, s.ProjectID, getAutocompleteType("REVID", s.Platform), s.RevID) // s.Platform == "web" - conn.insertAutocompleteValue(sessionID, "USERBROWSER", s.UserBrowser) + conn.insertAutocompleteValue(sessionID, s.ProjectID, "USERBROWSER", s.UserBrowser) return nil } +func (conn *Conn) GetSessionDuration(sessionID uint64) (uint64, error) { + var dur uint64 + if err := conn.c.QueryRow("SELECT COALESCE( duration, 0 ) FROM sessions WHERE session_id=$1", sessionID).Scan(&dur); err != nil { + return 0, err + } + return dur, nil +} + func (conn *Conn) InsertSessionEnd(sessionID uint64, timestamp uint64) (uint64, error) { var dur uint64 - if err := conn.queryRow(` + if err := conn.c.QueryRow(` UPDATE sessions SET duration=$2 - start_ts WHERE session_id=$1 RETURNING duration @@ -119,30 +104,16 @@ func (conn *Conn) HandleSessionEnd(sessionID uint64) error { } func (conn *Conn) InsertRequest(sessionID uint64, timestamp uint64, index uint64, url string, duration uint64, success bool) error { - sqlRequest := ` - INSERT INTO events_common.requests ( - session_id, timestamp, seq_index, url, duration, success - ) VALUES ( - $1, $2, $3, left($4, 2700), $5, $6 - )` - conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), url, duration, success) - - // Record approximate message size - conn.updateBatchSize(sessionID, len(sqlRequest)+len(url)+8*4) + if err := conn.requests.Append(sessionID, timestamp, getSqIdx(index), url, duration, success); err != nil { + return fmt.Errorf("insert request in bulk err: %s", err) + } return nil } func (conn *Conn) InsertCustomEvent(sessionID uint64, timestamp uint64, index uint64, name string, payload string) error { - sqlRequest := ` - INSERT INTO events_common.customs ( - session_id, timestamp, seq_index, name, payload - ) VALUES ( - $1, $2, $3, left($4, 2700), $5 - )` - conn.batchQueue(sessionID, sqlRequest, sessionID, timestamp, getSqIdx(index), name, payload) - - // Record approximate message size - conn.updateBatchSize(sessionID, len(sqlRequest)+len(name)+len(payload)+8*3) + if err := conn.customEvents.Append(sessionID, timestamp, getSqIdx(index), name, payload); err != nil { + return fmt.Errorf("insert custom event in bulk err: %s", err) + } return nil } @@ -172,15 +143,21 @@ func (conn *Conn) InsertMetadata(sessionID uint64, keyNo uint, value string) err sqlRequest := ` UPDATE sessions SET metadata_%v = $1 WHERE session_id = $2` - return conn.exec(fmt.Sprintf(sqlRequest, keyNo), value, sessionID) + return conn.c.Exec(fmt.Sprintf(sqlRequest, keyNo), value, sessionID) } -func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messages.IssueEvent) error { - tx, err := conn.begin() +func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messages.IssueEvent) (err error) { + tx, err := conn.c.Begin() if err != nil { return err } - defer tx.rollback() + defer func() { + if err != nil { + if rollbackErr := tx.rollback(); rollbackErr != nil { + log.Printf("rollback err: %s", rollbackErr) + } + } + }() issueID := hashid.IssueID(projectID, e) // TEMP. TODO: nullable & json message field type @@ -237,5 +214,6 @@ func (conn *Conn) InsertIssueEvent(sessionID uint64, projectID uint32, e *messag return err } } - return tx.commit() + err = tx.commit() + return } diff --git a/backend/pkg/db/postgres/messages-ios.go b/backend/pkg/db/postgres/messages-ios.go index e75ff2acd..d7b2f58f3 100644 --- a/backend/pkg/db/postgres/messages-ios.go +++ b/backend/pkg/db/postgres/messages-ios.go @@ -9,7 +9,7 @@ import ( func (conn *Conn) InsertIOSCustomEvent(sessionID uint64, e *messages.IOSCustomEvent) error { err := conn.InsertCustomEvent(sessionID, e.Timestamp, e.Index, e.Name, e.Payload) if err == nil { - conn.insertAutocompleteValue(sessionID, "CUSTOM_IOS", e.Name) + conn.insertAutocompleteValue(sessionID, 0, "CUSTOM_IOS", e.Name) } return err } @@ -17,7 +17,7 @@ func (conn *Conn) InsertIOSCustomEvent(sessionID uint64, e *messages.IOSCustomEv func (conn *Conn) InsertIOSUserID(sessionID uint64, userID *messages.IOSUserID) error { err := conn.InsertUserID(sessionID, userID.Value) if err == nil { - conn.insertAutocompleteValue(sessionID, "USERID_IOS", userID.Value) + conn.insertAutocompleteValue(sessionID, 0, "USERID_IOS", userID.Value) } return err } @@ -25,7 +25,7 @@ func (conn *Conn) InsertIOSUserID(sessionID uint64, userID *messages.IOSUserID) func (conn *Conn) InsertIOSUserAnonymousID(sessionID uint64, userAnonymousID *messages.IOSUserAnonymousID) error { err := conn.InsertUserAnonymousID(sessionID, userAnonymousID.Value) if err == nil { - conn.insertAutocompleteValue(sessionID, "USERANONYMOUSID_IOS", userAnonymousID.Value) + conn.insertAutocompleteValue(sessionID, 0, "USERANONYMOUSID_IOS", userAnonymousID.Value) } return err } @@ -33,13 +33,13 @@ func (conn *Conn) InsertIOSUserAnonymousID(sessionID uint64, userAnonymousID *me func (conn *Conn) InsertIOSNetworkCall(sessionID uint64, e *messages.IOSNetworkCall) error { err := conn.InsertRequest(sessionID, e.Timestamp, e.Index, e.URL, e.Duration, e.Success) if err == nil { - conn.insertAutocompleteValue(sessionID, "REQUEST_IOS", url.DiscardURLQuery(e.URL)) + conn.insertAutocompleteValue(sessionID, 0, "REQUEST_IOS", url.DiscardURLQuery(e.URL)) } return err } func (conn *Conn) InsertIOSScreenEnter(sessionID uint64, screenEnter *messages.IOSScreenEnter) error { - tx, err := conn.begin() + tx, err := conn.c.Begin() if err != nil { return err } @@ -65,12 +65,12 @@ func (conn *Conn) InsertIOSScreenEnter(sessionID uint64, screenEnter *messages.I if err = tx.commit(); err != nil { return err } - conn.insertAutocompleteValue(sessionID, "VIEW_IOS", screenEnter.ViewName) + conn.insertAutocompleteValue(sessionID, 0, "VIEW_IOS", screenEnter.ViewName) return nil } func (conn *Conn) InsertIOSClickEvent(sessionID uint64, clickEvent *messages.IOSClickEvent) error { - tx, err := conn.begin() + tx, err := conn.c.Begin() if err != nil { return err } @@ -96,12 +96,12 @@ func (conn *Conn) InsertIOSClickEvent(sessionID uint64, clickEvent *messages.IOS if err = tx.commit(); err != nil { return err } - conn.insertAutocompleteValue(sessionID, "CLICK_IOS", clickEvent.Label) + conn.insertAutocompleteValue(sessionID, 0, "CLICK_IOS", clickEvent.Label) return nil } func (conn *Conn) InsertIOSInputEvent(sessionID uint64, inputEvent *messages.IOSInputEvent) error { - tx, err := conn.begin() + tx, err := conn.c.Begin() if err != nil { return err } @@ -132,13 +132,13 @@ func (conn *Conn) InsertIOSInputEvent(sessionID uint64, inputEvent *messages.IOS if err = tx.commit(); err != nil { return err } - conn.insertAutocompleteValue(sessionID, "INPUT_IOS", inputEvent.Label) + conn.insertAutocompleteValue(sessionID, 0, "INPUT_IOS", inputEvent.Label) // conn.insertAutocompleteValue(sessionID, "INPUT_VALUE", inputEvent.Label) return nil } func (conn *Conn) InsertIOSCrash(sessionID uint64, projectID uint32, crash *messages.IOSCrash) error { - tx, err := conn.begin() + tx, err := conn.c.Begin() if err != nil { return err } diff --git a/backend/pkg/db/postgres/messages-web.go b/backend/pkg/db/postgres/messages-web.go index e703ee933..c55344509 100644 --- a/backend/pkg/db/postgres/messages-web.go +++ b/backend/pkg/db/postgres/messages-web.go @@ -1,6 +1,7 @@ package postgres import ( + "log" "math" "openreplay/backend/pkg/hashid" @@ -13,104 +14,54 @@ func getSqIdx(messageID uint64) uint { return uint(messageID % math.MaxInt32) } -func (conn *Conn) InsertWebCustomEvent(sessionID uint64, e *CustomEvent) error { +func (conn *Conn) InsertWebCustomEvent(sessionID uint64, projectID uint32, e *CustomEvent) error { err := conn.InsertCustomEvent(sessionID, e.Timestamp, e.MessageID, e.Name, e.Payload) if err == nil { - conn.insertAutocompleteValue(sessionID, "CUSTOM", e.Name) + conn.insertAutocompleteValue(sessionID, projectID, "CUSTOM", e.Name) } return err } -func (conn *Conn) InsertWebUserID(sessionID uint64, userID *UserID) error { +func (conn *Conn) InsertWebUserID(sessionID uint64, projectID uint32, userID *UserID) error { err := conn.InsertUserID(sessionID, userID.ID) if err == nil { - conn.insertAutocompleteValue(sessionID, "USERID", userID.ID) + conn.insertAutocompleteValue(sessionID, projectID, "USERID", userID.ID) } return err } -func (conn *Conn) InsertWebUserAnonymousID(sessionID uint64, userAnonymousID *UserAnonymousID) error { +func (conn *Conn) InsertWebUserAnonymousID(sessionID uint64, projectID uint32, userAnonymousID *UserAnonymousID) error { err := conn.InsertUserAnonymousID(sessionID, userAnonymousID.ID) if err == nil { - conn.insertAutocompleteValue(sessionID, "USERANONYMOUSID", userAnonymousID.ID) + conn.insertAutocompleteValue(sessionID, projectID, "USERANONYMOUSID", userAnonymousID.ID) } return err } -// func (conn *Conn) InsertWebResourceEvent(sessionID uint64, e *ResourceEvent) error { -// if e.Type != "fetch" { -// return nil -// } -// err := conn.InsertRequest(sessionID, e.Timestamp, -// e.MessageID, -// e.URL, e.Duration, e.Success, -// ) -// if err == nil { -// conn.insertAutocompleteValue(sessionID, "REQUEST", url.DiscardURLQuery(e.URL)) -// } -// return err -// } - // TODO: fix column "dom_content_loaded_event_end" of relation "pages" -func (conn *Conn) InsertWebPageEvent(sessionID uint64, e *PageEvent) error { +func (conn *Conn) InsertWebPageEvent(sessionID uint64, projectID uint32, e *PageEvent) error { host, path, query, err := url.GetURLParts(e.URL) if err != nil { return err } - tx, err := conn.begin() - if err != nil { - return err + // base_path is deprecated + if err = conn.webPageEvents.Append(sessionID, e.MessageID, e.Timestamp, e.Referrer, url.DiscardURLQuery(e.Referrer), + host, path, query, e.DomContentLoadedEventEnd, e.LoadEventEnd, e.ResponseEnd, e.FirstPaint, e.FirstContentfulPaint, + e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive, calcResponseTime(e), calcDomBuildingTime(e)); err != nil { + log.Printf("insert web page event in bulk err: %s", err) } - defer tx.rollback() - // base_path is depricated - if err := tx.exec(` - INSERT INTO events.pages ( - session_id, message_id, timestamp, referrer, base_referrer, host, path, query, - dom_content_loaded_time, load_time, response_end, first_paint_time, first_contentful_paint_time, - speed_index, visually_complete, time_to_interactive, - response_time, dom_building_time - ) VALUES ( - $1, $2, $3, - $4, $5, - $6, $7, $8, - NULLIF($9, 0), NULLIF($10, 0), NULLIF($11, 0), NULLIF($12, 0), NULLIF($13, 0), - NULLIF($14, 0), NULLIF($15, 0), NULLIF($16, 0), - NULLIF($17, 0), NULLIF($18, 0) - ) - `, - sessionID, e.MessageID, e.Timestamp, - e.Referrer, url.DiscardURLQuery(e.Referrer), - host, path, query, - e.DomContentLoadedEventEnd, e.LoadEventEnd, e.ResponseEnd, e.FirstPaint, e.FirstContentfulPaint, - e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive, - calcResponseTime(e), calcDomBuildingTime(e), - ); err != nil { - return err - } - if err = tx.exec(` - UPDATE sessions SET (pages_count, events_count) = (pages_count + 1, events_count + 1) - WHERE session_id = $1`, - sessionID, - ); err != nil { - return err - } - if err = tx.commit(); err != nil { - return err - } - conn.insertAutocompleteValue(sessionID, "LOCATION", url.DiscardURLQuery(path)) - conn.insertAutocompleteValue(sessionID, "REFERRER", url.DiscardURLQuery(e.Referrer)) + // Accumulate session updates and exec inside batch with another sql commands + conn.updateSessionEvents(sessionID, 1, 1) + // Add new value set to autocomplete bulk + conn.insertAutocompleteValue(sessionID, projectID, "LOCATION", url.DiscardURLQuery(path)) + conn.insertAutocompleteValue(sessionID, projectID, "REFERRER", url.DiscardURLQuery(e.Referrer)) return nil } -func (conn *Conn) InsertWebClickEvent(sessionID uint64, e *ClickEvent) error { - tx, err := conn.begin() - if err != nil { - return err - } - defer tx.rollback() - if err = tx.exec(` +func (conn *Conn) InsertWebClickEvent(sessionID uint64, projectID uint32, e *ClickEvent) error { + sqlRequest := ` INSERT INTO events.clicks (session_id, message_id, timestamp, label, selector, url) (SELECT @@ -118,65 +69,40 @@ func (conn *Conn) InsertWebClickEvent(sessionID uint64, e *ClickEvent) error { FROM events.pages WHERE session_id = $1 AND timestamp <= $3 ORDER BY timestamp DESC LIMIT 1 ) - `, - sessionID, e.MessageID, e.Timestamp, e.Label, e.Selector, - ); err != nil { - return err - } - if err = tx.exec(` - UPDATE sessions SET events_count = events_count + 1 - WHERE session_id = $1`, - sessionID, - ); err != nil { - return err - } - if err = tx.commit(); err != nil { - return err - } - conn.insertAutocompleteValue(sessionID, "CLICK", e.Label) + ` + conn.batchQueue(sessionID, sqlRequest, sessionID, e.MessageID, e.Timestamp, e.Label, e.Selector) + // Accumulate session updates and exec inside batch with another sql commands + conn.updateSessionEvents(sessionID, 1, 0) + // Add new value set to autocomplete bulk + conn.insertAutocompleteValue(sessionID, projectID, "CLICK", e.Label) return nil } -func (conn *Conn) InsertWebInputEvent(sessionID uint64, e *InputEvent) error { - tx, err := conn.begin() - if err != nil { - return err - } - defer tx.rollback() +func (conn *Conn) InsertWebInputEvent(sessionID uint64, projectID uint32, e *InputEvent) error { value := &e.Value if e.ValueMasked { value = nil } - if err = tx.exec(` - INSERT INTO events.inputs - (session_id, message_id, timestamp, value, label) - VALUES - ($1, $2, $3, $4, NULLIF($5,'')) - `, - sessionID, e.MessageID, e.Timestamp, value, e.Label, - ); err != nil { - return err + if err := conn.webInputEvents.Append(sessionID, e.MessageID, e.Timestamp, value, e.Label); err != nil { + log.Printf("insert web input event err: %s", err) } - if err = tx.exec(` - UPDATE sessions SET events_count = events_count + 1 - WHERE session_id = $1`, - sessionID, - ); err != nil { - return err - } - if err = tx.commit(); err != nil { - return err - } - conn.insertAutocompleteValue(sessionID, "INPUT", e.Label) + conn.updateSessionEvents(sessionID, 1, 0) + conn.insertAutocompleteValue(sessionID, projectID, "INPUT", e.Label) return nil } -func (conn *Conn) InsertWebErrorEvent(sessionID uint64, projectID uint32, e *ErrorEvent) error { - tx, err := conn.begin() +func (conn *Conn) InsertWebErrorEvent(sessionID uint64, projectID uint32, e *ErrorEvent) (err error) { + tx, err := conn.c.Begin() if err != nil { return err } - defer tx.rollback() + defer func() { + if err != nil { + if rollbackErr := tx.rollback(); rollbackErr != nil { + log.Printf("rollback err: %s", rollbackErr) + } + } + }() errorID := hashid.WebErrorID(projectID, e) if err = tx.exec(` @@ -206,17 +132,18 @@ func (conn *Conn) InsertWebErrorEvent(sessionID uint64, projectID uint32, e *Err ); err != nil { return err } - return tx.commit() + err = tx.commit() + return } -func (conn *Conn) InsertWebFetchEvent(sessionID uint64, savePayload bool, e *FetchEvent) error { +func (conn *Conn) InsertWebFetchEvent(sessionID uint64, projectID uint32, savePayload bool, e *FetchEvent) error { var request, response *string if savePayload { request = &e.Request response = &e.Response } host, path, query, err := url.GetURLParts(e.URL) - conn.insertAutocompleteValue(sessionID, "REQUEST", path) + conn.insertAutocompleteValue(sessionID, projectID, "REQUEST", path) if err != nil { return err } @@ -246,29 +173,15 @@ func (conn *Conn) InsertWebFetchEvent(sessionID uint64, savePayload bool, e *Fet return nil } -func (conn *Conn) InsertWebGraphQLEvent(sessionID uint64, savePayload bool, e *GraphQLEvent) error { +func (conn *Conn) InsertWebGraphQLEvent(sessionID uint64, projectID uint32, savePayload bool, e *GraphQLEvent) error { var request, response *string if savePayload { request = &e.Variables response = &e.Response } - conn.insertAutocompleteValue(sessionID, "GRAPHQL", e.OperationName) - - sqlRequest := ` - INSERT INTO events.graphql ( - session_id, timestamp, message_id, - name, - request_body, response_body - ) VALUES ( - $1, $2, $3, - left($4, 2700), - $5, $6 - ) ON CONFLICT DO NOTHING` - conn.batchQueue(sessionID, sqlRequest, sessionID, e.Timestamp, e.MessageID, - e.OperationName, request, response, - ) - - // Record approximate message size - conn.updateBatchSize(sessionID, len(sqlRequest)+len(e.OperationName)+len(e.Variables)+len(e.Response)+8*3) + if err := conn.webGraphQLEvents.Append(sessionID, e.Timestamp, e.MessageID, e.OperationName, request, response); err != nil { + log.Printf("insert web graphQL event err: %s", err) + } + conn.insertAutocompleteValue(sessionID, projectID, "GRAPHQL", e.OperationName) return nil } diff --git a/backend/pkg/db/postgres/pool.go b/backend/pkg/db/postgres/pool.go new file mode 100644 index 000000000..5f9cbaa29 --- /dev/null +++ b/backend/pkg/db/postgres/pool.go @@ -0,0 +1,175 @@ +package postgres + +import ( + "context" + "errors" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric/instrument/syncfloat64" + "strings" + "time" +) + +// Pool is a pgx.Pool wrapper with metrics integration +type Pool interface { + Query(sql string, args ...interface{}) (pgx.Rows, error) + QueryRow(sql string, args ...interface{}) pgx.Row + Exec(sql string, arguments ...interface{}) error + SendBatch(b *pgx.Batch) pgx.BatchResults + Begin() (*_Tx, error) + Close() +} + +type poolImpl struct { + conn *pgxpool.Pool + sqlRequestTime syncfloat64.Histogram + sqlRequestCounter syncfloat64.Counter +} + +func (p *poolImpl) Query(sql string, args ...interface{}) (pgx.Rows, error) { + start := time.Now() + res, err := p.conn.Query(getTimeoutContext(), sql, args...) + method, table := methodName(sql) + p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", method), attribute.String("table", table)) + p.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", method), attribute.String("table", table)) + return res, err +} + +func (p *poolImpl) QueryRow(sql string, args ...interface{}) pgx.Row { + start := time.Now() + res := p.conn.QueryRow(getTimeoutContext(), sql, args...) + method, table := methodName(sql) + p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", method), attribute.String("table", table)) + p.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", method), attribute.String("table", table)) + return res +} + +func (p *poolImpl) Exec(sql string, arguments ...interface{}) error { + start := time.Now() + _, err := p.conn.Exec(getTimeoutContext(), sql, arguments...) + method, table := methodName(sql) + p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", method), attribute.String("table", table)) + p.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", method), attribute.String("table", table)) + return err +} + +func (p *poolImpl) SendBatch(b *pgx.Batch) pgx.BatchResults { + start := time.Now() + res := p.conn.SendBatch(getTimeoutContext(), b) + p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", "sendBatch")) + p.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", "sendBatch")) + return res +} + +func (p *poolImpl) Begin() (*_Tx, error) { + start := time.Now() + tx, err := p.conn.Begin(context.Background()) + p.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", "begin")) + p.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", "begin")) + return &_Tx{tx, p.sqlRequestTime, p.sqlRequestCounter}, err +} + +func (p *poolImpl) Close() { + p.conn.Close() +} + +func NewPool(conn *pgxpool.Pool, sqlRequestTime syncfloat64.Histogram, sqlRequestCounter syncfloat64.Counter) (Pool, error) { + if conn == nil { + return nil, errors.New("conn is empty") + } + return &poolImpl{ + conn: conn, + sqlRequestTime: sqlRequestTime, + sqlRequestCounter: sqlRequestCounter, + }, nil +} + +// TX - start + +type _Tx struct { + pgx.Tx + sqlRequestTime syncfloat64.Histogram + sqlRequestCounter syncfloat64.Counter +} + +func (tx *_Tx) exec(sql string, args ...interface{}) error { + start := time.Now() + _, err := tx.Exec(context.Background(), sql, args...) + method, table := methodName(sql) + tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", method), attribute.String("table", table)) + tx.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", method), attribute.String("table", table)) + return err +} + +func (tx *_Tx) rollback() error { + start := time.Now() + err := tx.Rollback(context.Background()) + tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", "rollback")) + tx.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", "rollback")) + return err +} + +func (tx *_Tx) commit() error { + start := time.Now() + err := tx.Commit(context.Background()) + tx.sqlRequestTime.Record(context.Background(), float64(time.Now().Sub(start).Milliseconds()), + attribute.String("method", "commit")) + tx.sqlRequestCounter.Add(context.Background(), 1, + attribute.String("method", "commit")) + return err +} + +// TX - end + +func getTimeoutContext() context.Context { + ctx, _ := context.WithTimeout(context.Background(), time.Second*30) + return ctx +} + +func methodName(sql string) (string, string) { + cmd, table := "unknown", "unknown" + + // Prepare sql request for parsing + sql = strings.TrimSpace(sql) + sql = strings.ReplaceAll(sql, "\n", " ") + sql = strings.ReplaceAll(sql, "\t", "") + sql = strings.ToLower(sql) + + // Get sql command name + parts := strings.Split(sql, " ") + if parts[0] == "" { + return cmd, table + } else { + cmd = strings.TrimSpace(parts[0]) + } + + // Get table name + switch cmd { + case "select": + for i, p := range parts { + if strings.TrimSpace(p) == "from" { + table = strings.TrimSpace(parts[i+1]) + } + } + case "update": + table = strings.TrimSpace(parts[1]) + case "insert": + table = strings.TrimSpace(parts[2]) + } + return cmd, table +} diff --git a/backend/pkg/db/postgres/project.go b/backend/pkg/db/postgres/project.go index 066339791..f38161885 100644 --- a/backend/pkg/db/postgres/project.go +++ b/backend/pkg/db/postgres/project.go @@ -6,7 +6,7 @@ import ( func (conn *Conn) GetProjectByKey(projectKey string) (*Project, error) { p := &Project{ProjectKey: projectKey} - if err := conn.queryRow(` + if err := conn.c.QueryRow(` SELECT max_session_duration, sample_rate, project_id FROM projects WHERE project_key=$1 AND active = true @@ -21,7 +21,7 @@ func (conn *Conn) GetProjectByKey(projectKey string) (*Project, error) { // TODO: logical separation of metadata func (conn *Conn) GetProject(projectID uint32) (*Project, error) { p := &Project{ProjectID: projectID} - if err := conn.queryRow(` + if err := conn.c.QueryRow(` SELECT project_key, max_session_duration, save_request_payloads, metadata_1, metadata_2, metadata_3, metadata_4, metadata_5, metadata_6, metadata_7, metadata_8, metadata_9, metadata_10 diff --git a/backend/pkg/db/postgres/session-updates.go b/backend/pkg/db/postgres/session-updates.go new file mode 100644 index 000000000..14260c2c6 --- /dev/null +++ b/backend/pkg/db/postgres/session-updates.go @@ -0,0 +1,30 @@ +package postgres + +// Mechanism of combination several session updates into one +const sessionUpdateReq = `UPDATE sessions SET (pages_count, events_count) = (pages_count + $1, events_count + $2) WHERE session_id = $3` + +type sessionUpdates struct { + sessionID uint64 + pages int + events int +} + +func NewSessionUpdates(sessionID uint64) *sessionUpdates { + return &sessionUpdates{ + sessionID: sessionID, + pages: 0, + events: 0, + } +} + +func (su *sessionUpdates) add(pages, events int) { + su.pages += pages + su.events += events +} + +func (su *sessionUpdates) request() (string, []interface{}) { + if su.pages == 0 && su.events == 0 { + return "", nil + } + return sessionUpdateReq, []interface{}{su.pages, su.events, su.sessionID} +} diff --git a/backend/pkg/db/postgres/session.go b/backend/pkg/db/postgres/session.go index 7148d9871..9735cdc1a 100644 --- a/backend/pkg/db/postgres/session.go +++ b/backend/pkg/db/postgres/session.go @@ -1,14 +1,11 @@ package postgres -//import . "openreplay/backend/pkg/messages" import . "openreplay/backend/pkg/db/types" -//import "log" - func (conn *Conn) GetSession(sessionID uint64) (*Session, error) { s := &Session{SessionID: sessionID} var revID, userOSVersion *string - if err := conn.queryRow(` + if err := conn.c.QueryRow(` SELECT platform, duration, project_id, start_ts, user_uuid, user_os, user_os_version, @@ -39,69 +36,3 @@ func (conn *Conn) GetSession(sessionID uint64) (*Session, error) { } return s, nil } - -// func (conn *Conn) GetSessionClickEvents(sessionID uint64) (list []IOSClickEvent, err error) { -// rows, err := conn.query(` -// SELECT -// timestamp, seq_index, label -// FROM events_ios.clicks -// WHERE session_id=$1 -// `, sessionID) -// if err != nil { -// return err -// } -// defer rows.Close() -// for rows.Next() { -// e := new(IOSClickEvent) -// if err = rows.Scan(&e.Timestamp, &e.Index, &e.Label); err != nil { -// log.Printf("Error while scanning click events: %v", err) -// } else { -// list = append(list, e) -// } -// } -// return list -// } - -// func (conn *Conn) GetSessionInputEvents(sessionID uint64) (list []IOSInputEvent, err error) { -// rows, err := conn.query(` -// SELECT -// timestamp, seq_index, label, value -// FROM events_ios.inputs -// WHERE session_id=$1 -// `, sessionID) -// if err != nil { -// return err -// } -// defer rows.Close() -// for rows.Next() { -// e := new(IOSInputEvent) -// if err = rows.Scan(&e.Timestamp, &e.Index, &e.Label, &e.Value); err != nil { -// log.Printf("Error while scanning click events: %v", err) -// } else { -// list = append(list, e) -// } -// } -// return list -// } - -// func (conn *Conn) GetSessionCrashEvents(sessionID uint64) (list []IOSCrash, err error) { -// rows, err := conn.query(` -// SELECT -// timestamp, seq_index -// FROM events_ios.crashes -// WHERE session_id=$1 -// `, sessionID) -// if err != nil { -// return err -// } -// defer rows.Close() -// for rows.Next() { -// e := new(IOSCrash) -// if err = rows.Scan(&e.Timestamp, &e.Index, &e.Label, &e.Value); err != nil { -// log.Printf("Error while scanning click events: %v", err) -// } else { -// list = append(list, e) -// } -// } -// return list -// } diff --git a/backend/pkg/db/postgres/unstarted-session.go b/backend/pkg/db/postgres/unstarted-session.go index 2a9a71037..cc27e3f5d 100644 --- a/backend/pkg/db/postgres/unstarted-session.go +++ b/backend/pkg/db/postgres/unstarted-session.go @@ -16,7 +16,7 @@ type UnstartedSession struct { } func (conn *Conn) InsertUnstartedSession(s UnstartedSession) error { - return conn.exec(` + return conn.c.Exec(` INSERT INTO unstarted_sessions ( project_id, tracker_version, do_not_track, diff --git a/backend/pkg/env/aws.go b/backend/pkg/env/aws.go index cb7445797..e25a3a561 100644 --- a/backend/pkg/env/aws.go +++ b/backend/pkg/env/aws.go @@ -1,7 +1,9 @@ package env import ( + "crypto/tls" "log" + "net/http" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" @@ -20,6 +22,15 @@ func AWSSessionOnRegion(region string) *_session.Session { config.Endpoint = aws.String(AWS_ENDPOINT) config.DisableSSL = aws.Bool(true) config.S3ForcePathStyle = aws.Bool(true) + + AWS_SKIP_SSL_VALIDATION := Bool("AWS_SKIP_SSL_VALIDATION") + if AWS_SKIP_SSL_VALIDATION { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + config.HTTPClient = client + } } aws_session, err := _session.NewSession(config) if err != nil { diff --git a/ee/api/Dockerfile b/ee/api/Dockerfile index 2500d2bfb..2e04fa330 100644 --- a/ee/api/Dockerfile +++ b/ee/api/Dockerfile @@ -1,7 +1,6 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec nodejs npm tini ARG envarg ENV SOURCE_MAP_VERSION=0.7.4 \ diff --git a/ee/api/Dockerfile.alerts b/ee/api/Dockerfile.alerts index 785b0a5f9..351fce661 100644 --- a/ee/api/Dockerfile.alerts +++ b/ee/api/Dockerfile.alerts @@ -1,7 +1,6 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base tini ARG envarg ENV APP_NAME=alerts \ diff --git a/ee/api/Dockerfile.crons b/ee/api/Dockerfile.crons index 0647c6fc6..96b9e6453 100644 --- a/ee/api/Dockerfile.crons +++ b/ee/api/Dockerfile.crons @@ -1,7 +1,6 @@ FROM python:3.10-alpine LABEL Maintainer="Rajesh Rajendran" LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache build-base tini ARG envarg ENV APP_NAME=crons \ diff --git a/ee/api/app.py b/ee/api/app.py index 505f1393c..9f2f9a306 100644 --- a/ee/api/app.py +++ b/ee/api/app.py @@ -5,18 +5,20 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from decouple import config from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware from starlette import status from starlette.responses import StreamingResponse, JSONResponse from chalicelib.utils import helper from chalicelib.utils import pg_client from routers import core, core_dynamic, ee, saml -from routers.subs import v1_api from routers.crons import core_crons from routers.crons import core_dynamic_crons from routers.subs import dashboard, insights, metrics, v1_api_ee +from routers.subs import v1_api -app = FastAPI(root_path="/api") +app = FastAPI(root_path="/api", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default="")) +app.add_middleware(GZipMiddleware, minimum_size=1000) @app.middleware('http') diff --git a/ee/api/chalicelib/core/integrations_global.py b/ee/api/chalicelib/core/integrations_global.py new file mode 100644 index 000000000..b923fc5ab --- /dev/null +++ b/ee/api/chalicelib/core/integrations_global.py @@ -0,0 +1,61 @@ +import schemas +from chalicelib.utils import pg_client + + +def get_global_integrations_status(tenant_id, user_id, project_id): + with pg_client.PostgresClient() as cur: + cur.execute( + cur.mogrify(f"""\ + SELECT EXISTS((SELECT 1 + FROM public.oauth_authentication + WHERE user_id = %(user_id)s + AND provider = 'github')) AS {schemas.IntegrationType.github}, + EXISTS((SELECT 1 + FROM public.jira_cloud + WHERE user_id = %(user_id)s)) AS {schemas.IntegrationType.jira}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='bugsnag')) AS {schemas.IntegrationType.bugsnag}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='cloudwatch')) AS {schemas.IntegrationType.cloudwatch}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='datadog')) AS {schemas.IntegrationType.datadog}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='newrelic')) AS {schemas.IntegrationType.newrelic}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='rollbar')) AS {schemas.IntegrationType.rollbar}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='sentry')) AS {schemas.IntegrationType.sentry}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='stackdriver')) AS {schemas.IntegrationType.stackdriver}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='sumologic')) AS {schemas.IntegrationType.sumologic}, + EXISTS((SELECT 1 + FROM public.integrations + WHERE project_id=%(project_id)s + AND provider='elasticsearch')) AS {schemas.IntegrationType.elasticsearch}, + EXISTS((SELECT 1 + FROM public.webhooks + WHERE type='slack' AND tenant_id=%(tenant_id)s)) AS {schemas.IntegrationType.slack};""", + {"user_id": user_id, "tenant_id": tenant_id, "project_id": project_id}) + ) + current_integrations = cur.fetchone() + result = [] + for k in current_integrations.keys(): + result.append({"name": k, "integrated": current_integrations[k]}) + return result diff --git a/ee/api/env.default b/ee/api/env.default index 7687566d7..70941ab99 100644 --- a/ee/api/env.default +++ b/ee/api/env.default @@ -57,3 +57,4 @@ sourcemaps_bucket=sourcemaps sourcemaps_reader=http://127.0.0.1:9000/sourcemaps stage=default-ee version_number=1.0.0 +FS_DIR=/mnt/efs \ No newline at end of file diff --git a/ee/api/requirements-alerts.txt b/ee/api/requirements-alerts.txt index 66fa84713..906189999 100644 --- a/ee/api/requirements-alerts.txt +++ b/ee/api/requirements-alerts.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/ee/api/requirements-crons.txt b/ee/api/requirements-crons.txt index 66fa84713..906189999 100644 --- a/ee/api/requirements-crons.txt +++ b/ee/api/requirements-crons.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/ee/api/requirements.txt b/ee/api/requirements.txt index 5ce044904..0a8ca819e 100644 --- a/ee/api/requirements.txt +++ b/ee/api/requirements.txt @@ -4,7 +4,7 @@ boto3==1.24.26 pyjwt==2.4.0 psycopg2-binary==2.9.3 elasticsearch==8.3.1 -jira==3.3.0 +jira==3.3.1 diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index 3c5c21905..9d09198a6 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -90,18 +90,6 @@ def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = B changes={"name": data.name, "endpoint": data.url})} -# this endpoint supports both jira & github based on `provider` attribute -@app.post('/integrations/issues', tags=["integrations"]) -def add_edit_jira_cloud_github(data: schemas.JiraGithubSchema, - context: schemas.CurrentContext = Depends(OR_context)): - provider = data.provider.upper() - error, integration = integrations_manager.get_integration(tool=provider, tenant_id=context.tenant_id, - user_id=context.user_id) - if error is not None: - return error - return {"data": integration.add_edit(data=data.dict())} - - @app.post('/client/members', tags=["client"]) @app.put('/client/members', tags=["client"]) def add_member(background_tasks: BackgroundTasks, data: schemas_ee.CreateMemberSchema = Body(...), diff --git a/ee/backend/internal/db/datasaver/stats.go b/ee/backend/internal/db/datasaver/stats.go index d5bd74f83..7fa2fb9d0 100644 --- a/ee/backend/internal/db/datasaver/stats.go +++ b/ee/backend/internal/db/datasaver/stats.go @@ -10,7 +10,7 @@ import ( . "openreplay/backend/pkg/messages" ) -var ch *clickhouse.Connector +var ch clickhouse.Connector var finalizeTicker <-chan time.Time func (si *Saver) InitStats() { diff --git a/ee/backend/pkg/db/clickhouse/bulk.go b/ee/backend/pkg/db/clickhouse/bulk.go deleted file mode 100644 index 121cdbbf0..000000000 --- a/ee/backend/pkg/db/clickhouse/bulk.go +++ /dev/null @@ -1,45 +0,0 @@ -package clickhouse - -import ( - "errors" - "database/sql" -) - -type bulk struct { - db *sql.DB - query string - tx *sql.Tx - stmt *sql.Stmt -} - -func newBulk(db *sql.DB, query string) *bulk { - return &bulk{ - db: db, - query: query, - } -} - -func (b *bulk) prepare() error { - var err error - b.tx, err = b.db.Begin() - if err != nil { - return err - } - b.stmt, err = b.tx.Prepare(b.query) - if err != nil { - return err - } - return nil -} - -func (b *bulk) commit() error { - return b.tx.Commit() -} - -func (b *bulk) exec(args ...interface{}) error { - if b.stmt == nil { - return errors.New("Bulk is not prepared.") - } - _, err := b.stmt.Exec(args...) - return err -} diff --git a/ee/backend/pkg/db/clickhouse/connector.go b/ee/backend/pkg/db/clickhouse/connector.go index cc0d20497..1fd6e5d1e 100644 --- a/ee/backend/pkg/db/clickhouse/connector.go +++ b/ee/backend/pkg/db/clickhouse/connector.go @@ -1,138 +1,416 @@ package clickhouse import ( - "database/sql" - _ "github.com/ClickHouse/clickhouse-go" + "context" + "errors" + "fmt" + "github.com/ClickHouse/clickhouse-go/v2" + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "log" + "openreplay/backend/pkg/db/types" + "openreplay/backend/pkg/hashid" + "openreplay/backend/pkg/messages" + "openreplay/backend/pkg/url" + "strings" + "time" "openreplay/backend/pkg/license" ) -type Connector struct { - sessions *bulk - metadata *bulk // TODO: join sessions, sessions_metadata & sessions_ios - resources *bulk - pages *bulk - clicks *bulk - inputs *bulk - errors *bulk - performance *bulk - longtasks *bulk - db *sql.DB +var CONTEXT_MAP = map[uint64]string{0: "unknown", 1: "self", 2: "same-origin-ancestor", 3: "same-origin-descendant", 4: "same-origin", 5: "cross-origin-ancestor", 6: "cross-origin-descendant", 7: "cross-origin-unreachable", 8: "multiple-contexts"} +var CONTAINER_TYPE_MAP = map[uint64]string{0: "window", 1: "iframe", 2: "embed", 3: "object"} + +type Connector interface { + Prepare() error + Commit() error + FinaliseSessionsTable() error + InsertWebSession(session *types.Session) error + InsertWebResourceEvent(session *types.Session, msg *messages.ResourceEvent) error + InsertWebPageEvent(session *types.Session, msg *messages.PageEvent) error + InsertWebClickEvent(session *types.Session, msg *messages.ClickEvent) error + InsertWebInputEvent(session *types.Session, msg *messages.InputEvent) error + InsertWebErrorEvent(session *types.Session, msg *messages.ErrorEvent) error + InsertWebPerformanceTrackAggr(session *types.Session, msg *messages.PerformanceTrackAggr) error + InsertLongtask(session *types.Session, msg *messages.LongTask) error } -func NewConnector(url string) *Connector { +type connectorImpl struct { + conn driver.Conn + batches map[string]driver.Batch +} + +func NewConnector(url string) Connector { license.CheckLicense() - - db, err := sql.Open("clickhouse", url) + url = strings.TrimPrefix(url, "tcp://") + url = strings.TrimSuffix(url, "/default") + conn, err := clickhouse.Open(&clickhouse.Options{ + Addr: []string{url}, + Auth: clickhouse.Auth{ + Database: "default", + }, + MaxOpenConns: 20, + MaxIdleConns: 15, + ConnMaxLifetime: 3 * time.Minute, + Compression: &clickhouse.Compression{ + Method: clickhouse.CompressionLZ4, + }, + // Debug: true, + }) if err != nil { - log.Fatalln(err) + log.Fatal(err) } - return &Connector{ - db: db, - sessions: newBulk(db, ` - INSERT INTO sessions (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, duration, pages_count, events_count, errors_count, user_browser, user_browser_version) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), - // TODO: join sessions, sessions_metadata & sessions_ios - metadata: newBulk(db, ` - INSERT INTO sessions_metadata (session_id, user_id, user_anonymous_id, metadata_1, metadata_2, metadata_3, metadata_4, metadata_5, metadata_6, metadata_7, metadata_8, metadata_9, metadata_10, datetime) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), - resources: newBulk(db, ` - INSERT INTO resources (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, url, type, duration, ttfb, header_size, encoded_body_size, decoded_body_size, success) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), - pages: newBulk(db, ` - INSERT INTO pages (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, url, 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), - clicks: newBulk(db, ` - INSERT INTO clicks (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, label, hesitation_time) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), - inputs: newBulk(db, ` - INSERT INTO inputs (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, label) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), - errors: newBulk(db, ` - INSERT INTO errors (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, source, name, message, error_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), - performance: newBulk(db, ` - INSERT INTO performance (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), - longtasks: newBulk(db, ` - INSERT INTO longtasks (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, context, container_type, container_id, container_name, container_src) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `), + + c := &connectorImpl{ + conn: conn, + batches: make(map[string]driver.Batch, 9), } + return c } -func (conn *Connector) Prepare() error { - if err := conn.sessions.prepare(); err != nil { - return err +func (c *connectorImpl) newBatch(name, query string) error { + batch, err := c.conn.PrepareBatch(context.Background(), query) + if err != nil { + return fmt.Errorf("can't create new batch: %s", err) } - if err := conn.metadata.prepare(); err != nil { - return err + if _, ok := c.batches[name]; ok { + delete(c.batches, name) } - if err := conn.resources.prepare(); err != nil { - return err - } - if err := conn.pages.prepare(); err != nil { - return err - } - if err := conn.clicks.prepare(); err != nil { - return err - } - if err := conn.inputs.prepare(); err != nil { - return err - } - if err := conn.errors.prepare(); err != nil { - return err - } - if err := conn.performance.prepare(); err != nil { - return err - } - if err := conn.longtasks.prepare(); err != nil { - return err + c.batches[name] = batch + return nil +} + +var batches = map[string]string{ + "sessions": "INSERT INTO sessions (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country, datetime, duration, pages_count, events_count, errors_count, user_browser, user_browser_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "metadata": "INSERT INTO sessions_metadata (session_id, user_id, user_anonymous_id, metadata_1, metadata_2, metadata_3, metadata_4, metadata_5, metadata_6, metadata_7, metadata_8, metadata_9, metadata_10, datetime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "resources": "INSERT INTO resources (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, url, type, duration, ttfb, header_size, encoded_body_size, decoded_body_size, success) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "pages": "INSERT INTO pages (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, url, 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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "clicks": "INSERT INTO clicks (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, label, hesitation_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "inputs": "INSERT INTO inputs (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, label) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "errors": "INSERT INTO errors (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, source, name, message, error_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "performance": "INSERT INTO performance (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, 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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "longtasks": "INSERT INTO longtasks (session_id, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_browser, user_browser_version, user_device, user_device_type, user_country, datetime, context, container_type, container_id, container_name, container_src) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", +} + +func (c *connectorImpl) Prepare() error { + for table, query := range batches { + if err := c.newBatch(table, query); err != nil { + return fmt.Errorf("can't create %s batch: %s", table, err) + } } return nil } -func (conn *Connector) Commit() error { - if err := conn.sessions.commit(); err != nil { - return err - } - if err := conn.metadata.commit(); err != nil { - return err - } - if err := conn.resources.commit(); err != nil { - return err - } - if err := conn.pages.commit(); err != nil { - return err - } - if err := conn.clicks.commit(); err != nil { - return err - } - if err := conn.inputs.commit(); err != nil { - return err - } - if err := conn.errors.commit(); err != nil { - return err - } - if err := conn.performance.commit(); err != nil { - return err - } - if err := conn.longtasks.commit(); err != nil { - return err +func (c *connectorImpl) Commit() error { + for _, b := range c.batches { + if err := b.Send(); err != nil { + return fmt.Errorf("can't send batch: %s", err) + } } return nil } -func (conn *Connector) FinaliseSessionsTable() error { - _, err := conn.db.Exec("OPTIMIZE TABLE sessions FINAL") - return err +func (c *connectorImpl) FinaliseSessionsTable() error { + if err := c.conn.Exec(context.Background(), "OPTIMIZE TABLE sessions FINAL"); err != nil { + return fmt.Errorf("can't finalise sessions table: %s", err) + } + return nil +} + +func (c *connectorImpl) checkError(name string, err error) { + if err != clickhouse.ErrBatchAlreadySent { + if batchErr := c.newBatch(name, batches[name]); batchErr != nil { + log.Printf("can't create %s batch after failed append operation: %s", name, batchErr) + } + } +} + +func (c *connectorImpl) InsertWebSession(session *types.Session) error { + if session.Duration == nil { + return errors.New("trying to insert session with nil duration") + } + if err := c.batches["sessions"].Append( + session.SessionID, + session.ProjectID, + session.TrackerVersion, + nullableString(session.RevID), + session.UserUUID, + session.UserOS, + nullableString(session.UserOSVersion), + nullableString(session.UserDevice), + session.UserDeviceType, + session.UserCountry, + datetime(session.Timestamp), + uint32(*session.Duration), + uint16(session.PagesCount), + uint16(session.EventsCount), + uint16(session.ErrorsCount), + // Web unique columns + session.UserBrowser, + nullableString(session.UserBrowserVersion), + ); err != nil { + c.checkError("sessions", err) + return fmt.Errorf("can't append to sessions batch: %s", err) + } + if err := c.batches["metadata"].Append( + session.SessionID, + session.UserID, + session.UserAnonymousID, + session.Metadata1, + session.Metadata2, + session.Metadata3, + session.Metadata4, + session.Metadata5, + session.Metadata6, + session.Metadata7, + session.Metadata8, + session.Metadata9, + session.Metadata10, + datetime(session.Timestamp), + ); err != nil { + c.checkError("metadata", err) + return fmt.Errorf("can't append to metadata batch: %s", err) + } + return nil +} + +func (c *connectorImpl) InsertWebResourceEvent(session *types.Session, msg *messages.ResourceEvent) error { + var method interface{} = url.EnsureMethod(msg.Method) + if method == "" { + method = nil + } + if err := c.batches["resources"].Append( + session.SessionID, + session.ProjectID, + session.TrackerVersion, + nullableString(session.RevID), + session.UserUUID, + session.UserOS, + nullableString(session.UserOSVersion), + session.UserBrowser, + nullableString(session.UserBrowserVersion), + nullableString(session.UserDevice), + session.UserDeviceType, + session.UserCountry, + datetime(msg.Timestamp), + url.DiscardURLQuery(msg.URL), + msg.Type, + nullableUint16(uint16(msg.Duration)), + nullableUint16(uint16(msg.TTFB)), + nullableUint16(uint16(msg.HeaderSize)), + nullableUint32(uint32(msg.EncodedBodySize)), + nullableUint32(uint32(msg.DecodedBodySize)), + msg.Success, + ); err != nil { + c.checkError("resources", err) + return fmt.Errorf("can't append to resources batch: %s", err) + } + return nil +} + +func (c *connectorImpl) InsertWebPageEvent(session *types.Session, msg *messages.PageEvent) error { + if err := c.batches["pages"].Append( + session.SessionID, + session.ProjectID, + session.TrackerVersion, nullableString(session.RevID), + session.UserUUID, + session.UserOS, + nullableString(session.UserOSVersion), + session.UserBrowser, + nullableString(session.UserBrowserVersion), + nullableString(session.UserDevice), + session.UserDeviceType, + session.UserCountry, + datetime(msg.Timestamp), + url.DiscardURLQuery(msg.URL), + nullableUint16(uint16(msg.RequestStart)), + nullableUint16(uint16(msg.ResponseStart)), + nullableUint16(uint16(msg.ResponseEnd)), + nullableUint16(uint16(msg.DomContentLoadedEventStart)), + nullableUint16(uint16(msg.DomContentLoadedEventEnd)), + nullableUint16(uint16(msg.LoadEventStart)), + nullableUint16(uint16(msg.LoadEventEnd)), + nullableUint16(uint16(msg.FirstPaint)), + nullableUint16(uint16(msg.FirstContentfulPaint)), + nullableUint16(uint16(msg.SpeedIndex)), + nullableUint16(uint16(msg.VisuallyComplete)), + nullableUint16(uint16(msg.TimeToInteractive)), + ); err != nil { + c.checkError("pages", err) + return fmt.Errorf("can't append to pages batch: %s", err) + } + return nil +} + +func (c *connectorImpl) InsertWebClickEvent(session *types.Session, msg *messages.ClickEvent) error { + if msg.Label == "" { + return nil + } + if err := c.batches["clicks"].Append( + session.SessionID, + session.ProjectID, + session.TrackerVersion, + nullableString(session.RevID), + session.UserUUID, + session.UserOS, + nullableString(session.UserOSVersion), + session.UserBrowser, + nullableString(session.UserBrowserVersion), + nullableString(session.UserDevice), + session.UserDeviceType, + session.UserCountry, + datetime(msg.Timestamp), + msg.Label, + nullableUint32(uint32(msg.HesitationTime)), + ); err != nil { + c.checkError("clicks", err) + return fmt.Errorf("can't append to clicks batch: %s", err) + } + return nil +} + +func (c *connectorImpl) InsertWebInputEvent(session *types.Session, msg *messages.InputEvent) error { + if msg.Label == "" { + return nil + } + if err := c.batches["inputs"].Append( + session.SessionID, + session.ProjectID, + session.TrackerVersion, + nullableString(session.RevID), + session.UserUUID, + session.UserOS, + nullableString(session.UserOSVersion), + session.UserBrowser, + nullableString(session.UserBrowserVersion), + nullableString(session.UserDevice), + session.UserDeviceType, + session.UserCountry, + datetime(msg.Timestamp), + msg.Label, + ); err != nil { + c.checkError("inputs", err) + return fmt.Errorf("can't append to inputs batch: %s", err) + } + return nil +} + +func (c *connectorImpl) InsertWebErrorEvent(session *types.Session, msg *messages.ErrorEvent) error { + if err := c.batches["errors"].Append( + session.SessionID, + session.ProjectID, + session.TrackerVersion, + nullableString(session.RevID), + session.UserUUID, + session.UserOS, + nullableString(session.UserOSVersion), + session.UserBrowser, + nullableString(session.UserBrowserVersion), + nullableString(session.UserDevice), + session.UserDeviceType, + session.UserCountry, + datetime(msg.Timestamp), + msg.Source, + nullableString(msg.Name), + msg.Message, + hashid.WebErrorID(session.ProjectID, msg), + ); err != nil { + c.checkError("errors", err) + return fmt.Errorf("can't append to errors batch: %s", err) + } + return nil +} + +func (c *connectorImpl) InsertWebPerformanceTrackAggr(session *types.Session, msg *messages.PerformanceTrackAggr) error { + var timestamp uint64 = (msg.TimestampStart + msg.TimestampEnd) / 2 + if err := c.batches["performance"].Append( + session.SessionID, + session.ProjectID, + session.TrackerVersion, + nullableString(session.RevID), + session.UserUUID, + session.UserOS, + nullableString(session.UserOSVersion), + session.UserBrowser, + nullableString(session.UserBrowserVersion), + nullableString(session.UserDevice), + session.UserDeviceType, + session.UserCountry, + datetime(timestamp), + uint8(msg.MinFPS), + uint8(msg.AvgFPS), + uint8(msg.MaxFPS), + uint8(msg.MinCPU), + uint8(msg.AvgCPU), + uint8(msg.MaxCPU), + msg.MinTotalJSHeapSize, + msg.AvgTotalJSHeapSize, + msg.MaxTotalJSHeapSize, + msg.MinUsedJSHeapSize, + msg.AvgUsedJSHeapSize, + msg.MaxUsedJSHeapSize, + ); err != nil { + c.checkError("performance", err) + return fmt.Errorf("can't append to performance batch: %s", err) + } + return nil +} + +func (c *connectorImpl) InsertLongtask(session *types.Session, msg *messages.LongTask) error { + if err := c.batches["longtasks"].Append( + session.SessionID, + session.ProjectID, + session.TrackerVersion, + nullableString(session.RevID), + session.UserUUID, + session.UserOS, + nullableString(session.UserOSVersion), + session.UserBrowser, + nullableString(session.UserBrowserVersion), + nullableString(session.UserDevice), + session.UserDeviceType, + session.UserCountry, + datetime(msg.Timestamp), + CONTEXT_MAP[msg.Context], + CONTAINER_TYPE_MAP[msg.ContainerType], + msg.ContainerId, + msg.ContainerName, + msg.ContainerSrc, + ); err != nil { + c.checkError("longtasks", err) + return fmt.Errorf("can't append to longtasks batch: %s", err) + } + return nil +} + +func nullableUint16(v uint16) *uint16 { + var p *uint16 = nil + if v != 0 { + p = &v + } + return p +} + +func nullableUint32(v uint32) *uint32 { + var p *uint32 = nil + if v != 0 { + p = &v + } + return p +} + +func nullableString(v string) *string { + var p *string = nil + if v != "" { + p = &v + } + return p +} + +func datetime(timestamp uint64) time.Time { + t := time.Unix(int64(timestamp/1e3), 0) + // Temporal solution for not correct timestamps in performance messages + if t.Year() < 2022 || t.Year() > 2025 { + return time.Now() + } + return t } diff --git a/ee/backend/pkg/db/clickhouse/helpers.go b/ee/backend/pkg/db/clickhouse/helpers.go deleted file mode 100644 index 37e30518c..000000000 --- a/ee/backend/pkg/db/clickhouse/helpers.go +++ /dev/null @@ -1,34 +0,0 @@ -package clickhouse - -import ( - "time" -) - - -func nullableUint16(v uint16) *uint16 { - var p *uint16 = nil - if v != 0 { - p = &v - } - return p -} - -func nullableUint32(v uint32) *uint32 { - var p *uint32 = nil - if v != 0 { - p = &v - } - return p -} - -func nullableString(v string) *string { - var p *string = nil - if v != "" { - p = &v - } - return p -} - -func datetime(timestamp uint64) time.Time { - return time.Unix(int64(timestamp/1e3), 0) -} diff --git a/ee/backend/pkg/db/clickhouse/messages-web.go b/ee/backend/pkg/db/clickhouse/messages-web.go deleted file mode 100644 index adfa38655..000000000 --- a/ee/backend/pkg/db/clickhouse/messages-web.go +++ /dev/null @@ -1,243 +0,0 @@ -package clickhouse - -import ( - "errors" - - . "openreplay/backend/pkg/db/types" - "openreplay/backend/pkg/hashid" - . "openreplay/backend/pkg/messages" - "openreplay/backend/pkg/url" -) - -func (conn *Connector) InsertWebSession(session *Session) error { - if session.Duration == nil { - return errors.New("Clickhouse: trying to insert session with ") - } - - if err := conn.sessions.exec( - session.SessionID, - session.ProjectID, - session.TrackerVersion, - nullableString(session.RevID), - session.UserUUID, - session.UserOS, - nullableString(session.UserOSVersion), - nullableString(session.UserDevice), - session.UserDeviceType, - session.UserCountry, - datetime(session.Timestamp), - uint32(*session.Duration), - session.PagesCount, - session.EventsCount, - session.ErrorsCount, - // Web unique columns - session.UserBrowser, - nullableString(session.UserBrowserVersion), - ); err != nil { - return err - } - // TODO: join sessions, sessions_metadata & sessions_ios - return conn.metadata.exec( - session.SessionID, - session.UserID, - session.UserAnonymousID, - session.Metadata1, - session.Metadata2, - session.Metadata3, - session.Metadata4, - session.Metadata5, - session.Metadata6, - session.Metadata7, - session.Metadata8, - session.Metadata9, - session.Metadata10, - datetime(session.Timestamp), - ) -} - -func (conn *Connector) InsertWebResourceEvent(session *Session, msg *ResourceEvent) error { - // nullableString causes error "unexpected type *string" on Nullable Enum type - // (apparently, a clickhouse-go bug) https://github.com/ClickHouse/clickhouse-go/pull/204 - var method interface{} = url.EnsureMethod(msg.Method) - if method == "" { - method = nil - } - return conn.resources.exec( - session.SessionID, - session.ProjectID, - session.TrackerVersion, - nullableString(session.RevID), - session.UserUUID, - session.UserOS, - nullableString(session.UserOSVersion), - session.UserBrowser, - nullableString(session.UserBrowserVersion), - nullableString(session.UserDevice), - session.UserDeviceType, - session.UserCountry, - datetime(msg.Timestamp), - url.DiscardURLQuery(msg.URL), - msg.Type, - nullableUint16(uint16(msg.Duration)), - nullableUint16(uint16(msg.TTFB)), - nullableUint16(uint16(msg.HeaderSize)), - nullableUint32(uint32(msg.EncodedBodySize)), - nullableUint32(uint32(msg.DecodedBodySize)), - msg.Success, - ) -} - -func (conn *Connector) InsertWebPageEvent(session *Session, msg *PageEvent) error { - return conn.pages.exec( - session.SessionID, - session.ProjectID, - session.TrackerVersion, nullableString(session.RevID), - session.UserUUID, - session.UserOS, - nullableString(session.UserOSVersion), - session.UserBrowser, - nullableString(session.UserBrowserVersion), - nullableString(session.UserDevice), - session.UserDeviceType, - session.UserCountry, - datetime(msg.Timestamp), - url.DiscardURLQuery(msg.URL), - nullableUint16(uint16(msg.RequestStart)), - nullableUint16(uint16(msg.ResponseStart)), - nullableUint16(uint16(msg.ResponseEnd)), - nullableUint16(uint16(msg.DomContentLoadedEventStart)), - nullableUint16(uint16(msg.DomContentLoadedEventEnd)), - nullableUint16(uint16(msg.LoadEventStart)), - nullableUint16(uint16(msg.LoadEventEnd)), - nullableUint16(uint16(msg.FirstPaint)), - nullableUint16(uint16(msg.FirstContentfulPaint)), - nullableUint16(uint16(msg.SpeedIndex)), - nullableUint16(uint16(msg.VisuallyComplete)), - nullableUint16(uint16(msg.TimeToInteractive)), - ) -} - -func (conn *Connector) InsertWebClickEvent(session *Session, msg *ClickEvent) error { - if msg.Label == "" { - return nil - } - return conn.clicks.exec( - session.SessionID, - session.ProjectID, - session.TrackerVersion, - nullableString(session.RevID), - session.UserUUID, - session.UserOS, - nullableString(session.UserOSVersion), - session.UserBrowser, - nullableString(session.UserBrowserVersion), - nullableString(session.UserDevice), - session.UserDeviceType, - session.UserCountry, - datetime(msg.Timestamp), - msg.Label, - nullableUint32(uint32(msg.HesitationTime)), - ) -} - -func (conn *Connector) InsertWebInputEvent(session *Session, msg *InputEvent) error { - if msg.Label == "" { - return nil - } - return conn.inputs.exec( - session.SessionID, - session.ProjectID, - session.TrackerVersion, - nullableString(session.RevID), - session.UserUUID, - session.UserOS, - nullableString(session.UserOSVersion), - session.UserBrowser, - nullableString(session.UserBrowserVersion), - nullableString(session.UserDevice), - session.UserDeviceType, - session.UserCountry, - datetime(msg.Timestamp), - msg.Label, - ) -} - -func (conn *Connector) InsertWebErrorEvent(session *Session, msg *ErrorEvent) error { - return conn.errors.exec( - session.SessionID, - session.ProjectID, - session.TrackerVersion, - nullableString(session.RevID), - session.UserUUID, - session.UserOS, - nullableString(session.UserOSVersion), - session.UserBrowser, - nullableString(session.UserBrowserVersion), - nullableString(session.UserDevice), - session.UserDeviceType, - session.UserCountry, - datetime(msg.Timestamp), - msg.Source, - nullableString(msg.Name), - msg.Message, - hashid.WebErrorID(session.ProjectID, msg), - ) -} - -func (conn *Connector) InsertWebPerformanceTrackAggr(session *Session, msg *PerformanceTrackAggr) error { - var timestamp uint64 = (msg.TimestampStart + msg.TimestampEnd) / 2 - return conn.performance.exec( - session.SessionID, - session.ProjectID, - session.TrackerVersion, - nullableString(session.RevID), - session.UserUUID, - session.UserOS, - nullableString(session.UserOSVersion), - session.UserBrowser, - nullableString(session.UserBrowserVersion), - nullableString(session.UserDevice), - session.UserDeviceType, - session.UserCountry, - datetime(timestamp), - uint8(msg.MinFPS), - uint8(msg.AvgFPS), - uint8(msg.MaxFPS), - uint8(msg.MinCPU), - uint8(msg.AvgCPU), - uint8(msg.MaxCPU), - msg.MinTotalJSHeapSize, - msg.AvgTotalJSHeapSize, - msg.MaxTotalJSHeapSize, - msg.MinUsedJSHeapSize, - msg.AvgUsedJSHeapSize, - msg.MaxUsedJSHeapSize, - ) -} - -// TODO: make enum message type -var CONTEXT_MAP = map[uint64]string{0: "unknown", 1: "self", 2: "same-origin-ancestor", 3: "same-origin-descendant", 4: "same-origin", 5: "cross-origin-ancestor", 6: "cross-origin-descendant", 7: "cross-origin-unreachable", 8: "multiple-contexts"} -var CONTAINER_TYPE_MAP = map[uint64]string{0: "window", 1: "iframe", 2: "embed", 3: "object"} - -func (conn *Connector) InsertLongtask(session *Session, msg *LongTask) error { - return conn.longtasks.exec( - session.SessionID, - session.ProjectID, - session.TrackerVersion, - nullableString(session.RevID), - session.UserUUID, - session.UserOS, - nullableString(session.UserOSVersion), - session.UserBrowser, - nullableString(session.UserBrowserVersion), - nullableString(session.UserDevice), - session.UserDeviceType, - session.UserCountry, - datetime(msg.Timestamp), - CONTEXT_MAP[msg.Context], - CONTAINER_TYPE_MAP[msg.ContainerType], - msg.ContainerId, - msg.ContainerName, - msg.ContainerSrc, - ) -} diff --git a/ee/connectors/consumer.py b/ee/connectors/consumer.py index dfa856501..8a233ecb6 100644 --- a/ee/connectors/consumer.py +++ b/ee/connectors/consumer.py @@ -49,7 +49,7 @@ def main(): elif LEVEL == 'normal': n = handle_normal_message(message) - session_id = codec.decode_key(msg.key) + session_id = decode_key(msg.key) sessions[session_id] = handle_session(sessions[session_id], message) if sessions[session_id]: sessions[session_id].sessionid = session_id @@ -116,6 +116,15 @@ def attempt_batch_insert(batch): except Exception as e: print(repr(e)) +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 if __name__ == '__main__': main() diff --git a/ee/connectors/main.py b/ee/connectors/main.py index 57349f6e9..ef3a824d9 100644 --- a/ee/connectors/main.py +++ b/ee/connectors/main.py @@ -49,7 +49,7 @@ def main(): elif LEVEL == 'normal': n = handle_normal_message(message) - session_id = codec.decode_key(msg.key) + session_id = decode_key(msg.key) sessions[session_id] = handle_session(sessions[session_id], message) if sessions[session_id]: sessions[session_id].sessionid = session_id @@ -116,6 +116,15 @@ def attempt_batch_insert(batch): except Exception as e: print(repr(e)) +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 if __name__ == '__main__': main() diff --git a/ee/connectors/msgcodec/codec.py b/ee/connectors/msgcodec/codec.py index 18f074a33..5aeb0e4ed 100644 --- a/ee/connectors/msgcodec/codec.py +++ b/ee/connectors/msgcodec/codec.py @@ -1,8 +1,5 @@ import io -from msgcodec.messages import * - - class Codec: """ Implements encode/decode primitives @@ -63,608 +60,3 @@ class Codec: 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 index c6e53b445..b500424b7 100644 --- a/ee/connectors/msgcodec/messages.py +++ b/ee/connectors/msgcodec/messages.py @@ -1,13 +1,20 @@ -""" -Representations of Kafka messages -""" -from abc import ABC +# Auto-generated, do not edit +from abc import ABC class Message(ABC): pass +class BatchMeta(Message): + __id__ = 80 + + def __init__(self, page_no, first_index, timestamp): + self.page_no = page_no + self.first_index = first_index + self.timestamp = timestamp + + class Timestamp(Message): __id__ = 0 @@ -18,10 +25,7 @@ class Timestamp(Message): 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): + 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, user_id): self.timestamp = timestamp self.project_id = project_id self.tracker_version = tracker_version @@ -37,6 +41,7 @@ class SessionStart(Message): self.user_device_memory_size = user_device_memory_size self.user_device_heap_size = user_device_heap_size self.user_country = user_country + self.user_id = user_id class SessionDisconnect(Message): @@ -48,7 +53,6 @@ class SessionDisconnect(Message): class SessionEnd(Message): __id__ = 3 - __name__ = 'SessionEnd' def __init__(self, timestamp): self.timestamp = timestamp @@ -82,13 +86,16 @@ class SetViewportScroll(Message): class CreateDocument(Message): __id__ = 7 + def __init__(self, ): + + class CreateElementNode(Message): __id__ = 8 def __init__(self, id, parent_id, index, tag, svg): self.id = id - self.parent_id = parent_id, + self.parent_id = parent_id self.index = index self.tag = tag self.svg = svg @@ -122,7 +129,7 @@ class RemoveNode(Message): class SetNodeAttribute(Message): __id__ = 12 - def __init__(self, id, name: str, value: str): + def __init__(self, id, name, value): self.id = id self.name = name self.value = value @@ -131,7 +138,7 @@ class SetNodeAttribute(Message): class RemoveNodeAttribute(Message): __id__ = 13 - def __init__(self, id, name: str): + def __init__(self, id, name): self.id = id self.name = name @@ -139,7 +146,7 @@ class RemoveNodeAttribute(Message): class SetNodeData(Message): __id__ = 14 - def __init__(self, id, data: str): + def __init__(self, id, data): self.id = id self.data = data @@ -147,7 +154,7 @@ class SetNodeData(Message): class SetCSSData(Message): __id__ = 15 - def __init__(self, id, data: str): + def __init__(self, id, data): self.id = id self.data = data @@ -155,7 +162,7 @@ class SetCSSData(Message): class SetNodeScroll(Message): __id__ = 16 - def __init__(self, id, x: int, y: int): + def __init__(self, id, x, y): self.id = id self.x = x self.y = y @@ -164,7 +171,7 @@ class SetNodeScroll(Message): class SetInputTarget(Message): __id__ = 17 - def __init__(self, id, label: str): + def __init__(self, id, label): self.id = id self.label = label @@ -172,7 +179,7 @@ class SetInputTarget(Message): class SetInputValue(Message): __id__ = 18 - def __init__(self, id, value: str, mask: int): + def __init__(self, id, value, mask): self.id = id self.value = value self.mask = mask @@ -181,7 +188,7 @@ class SetInputValue(Message): class SetInputChecked(Message): __id__ = 19 - def __init__(self, id, checked: bool): + def __init__(self, id, checked): self.id = id self.checked = checked @@ -194,10 +201,10 @@ class MouseMove(Message): self.y = y -class MouseClick(Message): +class MouseClickDepricated(Message): __id__ = 21 - def __init__(self, id, hesitation_time, label: str): + def __init__(self, id, hesitation_time, label): self.id = id self.hesitation_time = hesitation_time self.label = label @@ -206,7 +213,7 @@ class MouseClick(Message): class ConsoleLog(Message): __id__ = 22 - def __init__(self, level: str, value: str): + def __init__(self, level, value): self.level = level self.value = value @@ -214,9 +221,7 @@ class ConsoleLog(Message): 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): + 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 @@ -236,20 +241,20 @@ class PageRenderTiming(Message): 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): + def __init__(self, name, message, payload): self.name = name self.message = message self.payload = payload -class RawErrorEvent(Message): +class IntegrationEvent(Message): __id__ = 26 - def __init__(self, timestamp, source: str, name: str, message: str, - payload: str): + def __init__(self, timestamp, source, name, message, payload): self.timestamp = timestamp self.source = source self.name = name @@ -260,7 +265,7 @@ class RawErrorEvent(Message): class RawCustomEvent(Message): __id__ = 27 - def __init__(self, name: str, payload: str): + def __init__(self, name, payload): self.name = name self.payload = payload @@ -268,44 +273,29 @@ class RawCustomEvent(Message): class UserID(Message): __id__ = 28 - def __init__(self, id: str): + def __init__(self, id): self.id = id class UserAnonymousID(Message): __id__ = 29 - def __init__(self, id: str): + def __init__(self, id): self.id = id class Metadata(Message): __id__ = 30 - def __init__(self, key: str, value: str): + def __init__(self, key, value): 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): + def __init__(self, message_id, timestamp, url, referrer, loaded, 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 @@ -328,7 +318,7 @@ class PageEvent(Message): class InputEvent(Message): __id__ = 32 - def __init__(self, message_id, timestamp, value: str, value_masked: bool, label: str): + def __init__(self, message_id, timestamp, value, value_masked, label): self.message_id = message_id self.timestamp = timestamp self.value = value @@ -339,18 +329,18 @@ class InputEvent(Message): class ClickEvent(Message): __id__ = 33 - def __init__(self, message_id, timestamp, hesitation_time, label: str): + def __init__(self, message_id, timestamp, hesitation_time, label, selector): self.message_id = message_id self.timestamp = timestamp self.hesitation_time = hesitation_time self.label = label + self.selector = selector class ErrorEvent(Message): __id__ = 34 - def __init__(self, message_id, timestamp, source: str, name: str, message: str, - payload: str): + def __init__(self, message_id, timestamp, source, name, message, payload): self.message_id = message_id self.timestamp = timestamp self.source = source @@ -362,8 +352,7 @@ class ErrorEvent(Message): 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): + def __init__(self, message_id, timestamp, duration, ttfb, header_size, encoded_body_size, decoded_body_size, url, type, success, method, status): self.message_id = message_id self.timestamp = timestamp self.duration = duration @@ -381,7 +370,7 @@ class ResourceEvent(Message): class CustomEvent(Message): __id__ = 36 - def __init__(self, message_id, timestamp, name: str, payload: str): + def __init__(self, message_id, timestamp, name, payload): self.message_id = message_id self.timestamp = timestamp self.name = name @@ -391,7 +380,7 @@ class CustomEvent(Message): class CSSInsertRule(Message): __id__ = 37 - def __init__(self, id, rule: str, index): + def __init__(self, id, rule, index): self.id = id self.rule = rule self.index = index @@ -408,8 +397,7 @@ class CSSDeleteRule(Message): class Fetch(Message): __id__ = 39 - def __init__(self, method: str, url: str, request: str, response: str, status, - timestamp, duration): + def __init__(self, method, url, request, response, status, timestamp, duration): self.method = method self.url = url self.request = request @@ -422,7 +410,7 @@ class Fetch(Message): class Profiler(Message): __id__ = 40 - def __init__(self, name: str, duration, args: str, result: str): + def __init__(self, name, duration, args, result): self.name = name self.duration = duration self.args = args @@ -432,7 +420,7 @@ class Profiler(Message): class OTable(Message): __id__ = 41 - def __init__(self, key: str, value: str): + def __init__(self, key, value): self.key = key self.value = value @@ -440,14 +428,14 @@ class OTable(Message): class StateAction(Message): __id__ = 42 - def __init__(self, type: str): + def __init__(self, type): self.type = type class StateActionEvent(Message): __id__ = 43 - def __init__(self, message_id, timestamp, type: str): + def __init__(self, message_id, timestamp, type): self.message_id = message_id self.timestamp = timestamp self.type = type @@ -456,7 +444,7 @@ class StateActionEvent(Message): class Redux(Message): __id__ = 44 - def __init__(self, action: str, state: str, duration): + def __init__(self, action, state, duration): self.action = action self.state = state self.duration = duration @@ -465,7 +453,7 @@ class Redux(Message): class Vuex(Message): __id__ = 45 - def __init__(self, mutation: str, state: str): + def __init__(self, mutation, state): self.mutation = mutation self.state = state @@ -473,7 +461,7 @@ class Vuex(Message): class MobX(Message): __id__ = 46 - def __init__(self, type: str, payload: str): + def __init__(self, type, payload): self.type = type self.payload = payload @@ -481,7 +469,7 @@ class MobX(Message): class NgRx(Message): __id__ = 47 - def __init__(self, action: str, state: str, duration): + def __init__(self, action, state, duration): self.action = action self.state = state self.duration = duration @@ -490,8 +478,7 @@ class NgRx(Message): class GraphQL(Message): __id__ = 48 - def __init__(self, operation_kind: str, operation_name: str, - variables: str, response: str): + def __init__(self, operation_kind, operation_name, variables, response): self.operation_kind = operation_kind self.operation_name = operation_name self.variables = variables @@ -501,8 +488,7 @@ class GraphQL(Message): class PerformanceTrack(Message): __id__ = 49 - def __init__(self, frames: int, ticks: int, - total_js_heap_size, used_js_heap_size): + def __init__(self, frames, ticks, total_js_heap_size, used_js_heap_size): self.frames = frames self.ticks = ticks self.total_js_heap_size = total_js_heap_size @@ -512,13 +498,30 @@ class PerformanceTrack(Message): class GraphQLEvent(Message): __id__ = 50 - def __init__(self, message_id, timestamp, name: str): + def __init__(self, message_id, timestamp, operation_kind, operation_name, variables, response): self.message_id = message_id self.timestamp = timestamp - self.name = name + self.operation_kind = operation_kind + self.operation_name = operation_name + self.variables = variables + self.response = response -class DomDrop(Message): +class FetchEvent(Message): + __id__ = 51 + + def __init__(self, message_id, timestamp, method, url, request, response, status, duration): + self.message_id = message_id + self.timestamp = timestamp + self.method = method + self.url = url + self.request = request + self.response = response + self.status = status + self.duration = duration + + +class DOMDrop(Message): __id__ = 52 def __init__(self, timestamp): @@ -528,8 +531,7 @@ class DomDrop(Message): class ResourceTiming(Message): __id__ = 53 - def __init__(self, timestamp, duration, ttfb, header_size, encoded_body_size, - decoded_body_size, url, initiator): + 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 @@ -543,7 +545,7 @@ class ResourceTiming(Message): class ConnectionInformation(Message): __id__ = 54 - def __init__(self, downlink, type: str): + def __init__(self, downlink, type): self.downlink = downlink self.type = type @@ -551,19 +553,14 @@ class ConnectionInformation(Message): class SetPageVisibility(Message): __id__ = 55 - def __init__(self, hidden: bool): + def __init__(self, hidden): 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 - ): + 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 @@ -583,8 +580,7 @@ class PerformanceTrackAggr(Message): class LongTask(Message): __id__ = 59 - def __init__(self, timestamp, duration, context, container_type, container_src: str, - container_id: str, container_name: str): + def __init__(self, timestamp, duration, context, container_type, container_src, container_id, container_name): self.timestamp = timestamp self.duration = duration self.context = context @@ -594,20 +590,20 @@ class LongTask(Message): self.container_name = container_name -class SetNodeURLBasedAttribute(Message): +class SetNodeAttributeURLBased(Message): __id__ = 60 - def __init__(self, id, name: str, value: str, base_url: str): + def __init__(self, id, name, value, base_url): self.id = id self.name = name self.value = value self.base_url = base_url -class SetStyleData(Message): +class SetCSSDataURLBased(Message): __id__ = 61 - def __init__(self, id, data: str, base_url: str): + def __init__(self, id, data, base_url): self.id = id self.data = data self.base_url = base_url @@ -616,8 +612,7 @@ class SetStyleData(Message): class IssueEvent(Message): __id__ = 62 - def __init__(self, message_id, timestamp, type: str, context_string: str, - context: str, payload: str): + def __init__(self, message_id, timestamp, type, context_string, context, payload): self.message_id = message_id self.timestamp = timestamp self.type = type @@ -629,7 +624,7 @@ class IssueEvent(Message): class TechnicalInfo(Message): __id__ = 63 - def __init__(self, type: str, value: str): + def __init__(self, type, value): self.type = type self.value = value @@ -637,7 +632,7 @@ class TechnicalInfo(Message): class CustomIssue(Message): __id__ = 64 - def __init__(self, name: str, payload: str): + def __init__(self, name, payload): self.name = name self.payload = payload @@ -645,13 +640,58 @@ class CustomIssue(Message): class PageClose(Message): __id__ = 65 + def __init__(self, ): + + + +class AssetCache(Message): + __id__ = 66 + + def __init__(self, url): + self.url = url + + +class CSSInsertRuleURLBased(Message): + __id__ = 67 + + def __init__(self, id, rule, index, base_url): + self.id = id + self.rule = rule + self.index = index + self.base_url = base_url + + +class MouseClick(Message): + __id__ = 69 + + def __init__(self, id, hesitation_time, label, selector): + self.id = id + self.hesitation_time = hesitation_time + self.label = label + self.selector = selector + + +class CreateIFrameDocument(Message): + __id__ = 70 + + def __init__(self, frame_id, id): + self.frame_id = frame_id + self.id = id + + +class IOSBatchMeta(Message): + __id__ = 107 + + def __init__(self, timestamp, length, first_index): + self.timestamp = timestamp + self.length = length + self.first_index = first_index + 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): + def __init__(self, timestamp, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country): self.timestamp = timestamp self.project_id = project_id self.tracker_version = tracker_version @@ -674,17 +714,27 @@ class IOSSessionEnd(Message): class IOSMetadata(Message): __id__ = 92 - def __init__(self, timestamp, length, key: str, value: str): + def __init__(self, timestamp, length, key, value): self.timestamp = timestamp self.length = length self.key = key self.value = value +class IOSCustomEvent(Message): + __id__ = 93 + + def __init__(self, timestamp, length, name, payload): + self.timestamp = timestamp + self.length = length + self.name = name + self.payload = payload + + class IOSUserID(Message): __id__ = 94 - def __init__(self, timestamp, length, value: str): + def __init__(self, timestamp, length, value): self.timestamp = timestamp self.length = length self.value = value @@ -693,26 +743,91 @@ class IOSUserID(Message): class IOSUserAnonymousID(Message): __id__ = 95 - def __init__(self, timestamp, length, value: str): + def __init__(self, timestamp, length, value): self.timestamp = timestamp self.length = length self.value = value -class IOSScreenLeave(Message): - __id__ = 99 +class IOSScreenChanges(Message): + __id__ = 96 - def __init__(self, timestamp, length, title: str, view_name: str): + def __init__(self, timestamp, length, x, y, width, height): + self.timestamp = timestamp + self.length = length + self.x = x + self.y = y + self.width = width + self.height = height + + +class IOSCrash(Message): + __id__ = 97 + + def __init__(self, timestamp, length, name, reason, stacktrace): + self.timestamp = timestamp + self.length = length + self.name = name + self.reason = reason + self.stacktrace = stacktrace + + +class IOSScreenEnter(Message): + __id__ = 98 + + def __init__(self, timestamp, length, title, view_name): self.timestamp = timestamp self.length = length self.title = title self.view_name = view_name +class IOSScreenLeave(Message): + __id__ = 99 + + def __init__(self, timestamp, length, title, view_name): + self.timestamp = timestamp + self.length = length + self.title = title + self.view_name = view_name + + +class IOSClickEvent(Message): + __id__ = 100 + + def __init__(self, timestamp, length, label, x, y): + self.timestamp = timestamp + self.length = length + self.label = label + self.x = x + self.y = y + + +class IOSInputEvent(Message): + __id__ = 101 + + def __init__(self, timestamp, length, value, value_masked, label): + self.timestamp = timestamp + self.length = length + self.value = value + self.value_masked = value_masked + self.label = label + + +class IOSPerformanceEvent(Message): + __id__ = 102 + + def __init__(self, timestamp, length, name, value): + self.timestamp = timestamp + self.length = length + self.name = name + self.value = value + + class IOSLog(Message): __id__ = 103 - def __init__(self, timestamp, length, severity: str, content: str): + def __init__(self, timestamp, length, severity, content): self.timestamp = timestamp self.length = length self.severity = severity @@ -722,20 +837,31 @@ class IOSLog(Message): class IOSInternalError(Message): __id__ = 104 - def __init__(self, timestamp, length, content: str): + def __init__(self, timestamp, length, content): self.timestamp = timestamp self.length = length self.content = content +class IOSNetworkCall(Message): + __id__ = 105 + + def __init__(self, timestamp, length, duration, headers, body, url, success, method, status): + self.timestamp = timestamp + self.length = length + self.duration = duration + self.headers = headers + self.body = body + self.url = url + self.success = success + self.method = method + self.status = status + + 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 - ): + 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 @@ -750,3 +876,16 @@ class IOSPerformanceAggregated(Message): self.min_battery = min_battery self.avg_battery = avg_battery self.max_battery = max_battery + + +class IOSIssueEvent(Message): + __id__ = 111 + + def __init__(self, timestamp, type, context_string, context, payload): + self.timestamp = timestamp + self.type = type + self.context_string = context_string + self.context = context + self.payload = payload + + diff --git a/ee/connectors/msgcodec/msgcodec.py b/ee/connectors/msgcodec/msgcodec.py new file mode 100644 index 000000000..3bd74499c --- /dev/null +++ b/ee/connectors/msgcodec/msgcodec.py @@ -0,0 +1,728 @@ +# Auto-generated, do not edit + +from msgcodec.codec import Codec +from msgcodec.messages import * + +class MessageCodec(Codec): + + 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_ + + 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 == 80: + return BatchMeta( + page_no=self.read_uint(reader), + first_index=self.read_uint(reader), + timestamp=self.read_int(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), + user_id=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 MouseClickDepricated( + 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 IntegrationEvent( + 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), + selector=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: + return ResourceEvent( + message_id=self.read_uint(reader), + 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), + 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), + 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 == 51: + return FetchEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + 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), + duration=self.read_uint(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 SetNodeAttributeURLBased( + 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 SetCSSDataURLBased( + 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 == 66: + return AssetCache( + url=self.read_string(reader) + ) + + if message_id == 67: + return CSSInsertRuleURLBased( + id=self.read_uint(reader), + rule=self.read_string(reader), + index=self.read_uint(reader), + base_url=self.read_string(reader) + ) + + if message_id == 69: + return MouseClick( + id=self.read_uint(reader), + hesitation_time=self.read_uint(reader), + label=self.read_string(reader), + selector=self.read_string(reader) + ) + + if message_id == 70: + return CreateIFrameDocument( + frame_id=self.read_uint(reader), + id=self.read_uint(reader) + ) + + if message_id == 107: + return IOSBatchMeta( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + first_index=self.read_uint(reader) + ) + + 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 == 93: + return IOSCustomEvent( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + name=self.read_string(reader), + payload=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 == 96: + return IOSScreenChanges( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + x=self.read_uint(reader), + y=self.read_uint(reader), + width=self.read_uint(reader), + height=self.read_uint(reader) + ) + + if message_id == 97: + return IOSCrash( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + name=self.read_string(reader), + reason=self.read_string(reader), + stacktrace=self.read_string(reader) + ) + + if message_id == 98: + return IOSScreenEnter( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + title=self.read_string(reader), + view_name=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 == 100: + return IOSClickEvent( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + label=self.read_string(reader), + x=self.read_uint(reader), + y=self.read_uint(reader) + ) + + if message_id == 101: + return IOSInputEvent( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + value=self.read_string(reader), + value_masked=self.read_boolean(reader), + label=self.read_string(reader) + ) + + if message_id == 102: + return IOSPerformanceEvent( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + name=self.read_string(reader), + value=self.read_uint(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 == 105: + return IOSNetworkCall( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + duration=self.read_uint(reader), + headers=self.read_string(reader), + body=self.read_string(reader), + url=self.read_string(reader), + success=self.read_boolean(reader), + method=self.read_string(reader), + status=self.read_uint(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) + ) + + if message_id == 111: + return IOSIssueEvent( + 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) + ) + 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 e236ce90e..dfae901c5 100644 --- a/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql +++ b/ee/scripts/helm/db/init_dbs/postgresql/init_schema.sql @@ -659,24 +659,24 @@ $$ ); CREATE UNIQUE INDEX IF NOT EXISTS autocomplete_unique_project_id_md5value_type_idx ON autocomplete (project_id, md5(value), type); - CREATE index IF NOT EXISTS autocomplete_project_id_idx ON autocomplete (project_id); + CREATE INDEX IF NOT EXISTS autocomplete_project_id_idx ON autocomplete (project_id); CREATE INDEX IF NOT EXISTS autocomplete_type_idx ON public.autocomplete (type); - CREATE INDEX autocomplete_value_clickonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'CLICK'; - CREATE INDEX autocomplete_value_customonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'CUSTOM'; - CREATE INDEX autocomplete_value_graphqlonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'GRAPHQL'; - CREATE INDEX autocomplete_value_inputonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'INPUT'; - CREATE INDEX autocomplete_value_locationonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'LOCATION'; - CREATE INDEX autocomplete_value_referreronly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'REFERRER'; - CREATE INDEX autocomplete_value_requestonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'REQUEST'; - CREATE INDEX autocomplete_value_revidonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'REVID'; - CREATE INDEX autocomplete_value_stateactiononly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'STATEACTION'; - CREATE INDEX autocomplete_value_useranonymousidonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERANONYMOUSID'; - CREATE INDEX autocomplete_value_userbrowseronly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERBROWSER'; - CREATE INDEX autocomplete_value_usercountryonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERCOUNTRY'; - CREATE INDEX autocomplete_value_userdeviceonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERDEVICE'; - CREATE INDEX autocomplete_value_useridonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERID'; - CREATE INDEX autocomplete_value_userosonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USEROS'; + CREATE INDEX IF NOT EXISTS autocomplete_value_clickonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'CLICK'; + CREATE INDEX IF NOT EXISTS autocomplete_value_customonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'CUSTOM'; + CREATE INDEX IF NOT EXISTS autocomplete_value_graphqlonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'GRAPHQL'; + CREATE INDEX IF NOT EXISTS autocomplete_value_inputonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'INPUT'; + CREATE INDEX IF NOT EXISTS autocomplete_value_locationonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'LOCATION'; + CREATE INDEX IF NOT EXISTS autocomplete_value_referreronly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'REFERRER'; + CREATE INDEX IF NOT EXISTS autocomplete_value_requestonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'REQUEST'; + CREATE INDEX IF NOT EXISTS autocomplete_value_revidonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'REVID'; + CREATE INDEX IF NOT EXISTS autocomplete_value_stateactiononly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'STATEACTION'; + CREATE INDEX IF NOT EXISTS autocomplete_value_useranonymousidonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERANONYMOUSID'; + CREATE INDEX IF NOT EXISTS autocomplete_value_userbrowseronly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERBROWSER'; + CREATE INDEX IF NOT EXISTS autocomplete_value_usercountryonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERCOUNTRY'; + CREATE INDEX IF NOT EXISTS autocomplete_value_userdeviceonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERDEVICE'; + CREATE INDEX IF NOT EXISTS autocomplete_value_useridonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USERID'; + CREATE INDEX IF NOT EXISTS autocomplete_value_userosonly_gin_idx ON public.autocomplete USING GIN (value gin_trgm_ops) WHERE type = 'USEROS'; BEGIN IF NOT EXISTS(SELECT * diff --git a/ee/utilities/Dockerfile b/ee/utilities/Dockerfile index 2de6197a2..b4592048d 100644 --- a/ee/utilities/Dockerfile +++ b/ee/utilities/Dockerfile @@ -1,6 +1,5 @@ FROM node:18-alpine LABEL Maintainer="KRAIEM Taha Yassine" -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main RUN apk add --no-cache tini git libc6-compat && ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2 ARG envarg diff --git a/ee/utilities/package-lock.json b/ee/utilities/package-lock.json index 8aa6c9196..6b9dbdf1c 100644 --- a/ee/utilities/package-lock.json +++ b/ee/utilities/package-lock.json @@ -112,9 +112,9 @@ "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" }, "node_modules/@types/node": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", - "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz", + "integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==" }, "node_modules/accepts": { "version": "1.3.8", @@ -1179,9 +1179,9 @@ "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" }, "@types/node": { - "version": "18.0.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.3.tgz", - "integrity": "sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==" + "version": "18.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz", + "integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg==" }, "accepts": { "version": "1.3.8", diff --git a/ee/utilities/utils/helper-ee.js b/ee/utilities/utils/helper-ee.js index 50b414b7a..86997f0c4 100644 --- a/ee/utilities/utils/helper-ee.js +++ b/ee/utilities/utils/helper-ee.js @@ -91,6 +91,7 @@ const extractPayloadFromRequest = async function (req, res) { return helper.extractPayloadFromRequest(req); } filters.filter = helper.objectToObjectOfArrays(filters.filter); + filters.filter = helper.transformFilters(filters.filter); debug && console.log("payload/filters:" + JSON.stringify(filters)) return Object.keys(filters).length > 0 ? filters : undefined; } diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 2ab0312ab..5e6c9b3b0 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -14,10 +14,10 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf # Default step in docker build FROM nginx:alpine LABEL maintainer=Rajesh -RUN apk upgrade busybox --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main COPY --from=builder /work/public /var/www/openreplay COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 8080 RUN chown -R nginx:nginx /var/cache/nginx && \ chown -R nginx:nginx /var/log/nginx && \ chown -R nginx:nginx /etc/nginx/conf.d && \ diff --git a/frontend/app/Router.js b/frontend/app/Router.js index f5dc4c593..acbdd9cbb 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -31,7 +31,7 @@ const LiveSessionPure = lazy(() => import('Components/Session/LiveSession')); const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding')); const ClientPure = lazy(() => import('Components/Client/Client')); const AssistPure = lazy(() => import('Components/Assist')); -const BugFinderPure = lazy(() => import('Components/BugFinder/BugFinder')); +const BugFinderPure = lazy(() => import('Components/Overview')); const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard')); const ErrorsPure = lazy(() => import('Components/Errors/Errors')); const FunnelDetailsPure = lazy(() => import('Components/Funnels/FunnelDetails')); diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index 1f85d5af9..33f7ffe66 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -25,6 +25,7 @@ const siteIdRequiredPaths = [ '/custom_metrics', '/dashboards', '/metrics', + '/unprocessed', // '/custom_metrics/sessions', ]; diff --git a/frontend/app/api_middleware.js b/frontend/app/api_middleware.js index a29a22eb6..8f9965ec5 100644 --- a/frontend/app/api_middleware.js +++ b/frontend/app/api_middleware.js @@ -1,3 +1,4 @@ +import logger from 'App/logger'; import APIClient from './api_client'; import { UPDATE, DELETE } from './duck/jwt'; @@ -28,8 +29,9 @@ export default store => next => (action) => { next({ type: UPDATE, data: jwt }); } }) - .catch(() => { - return next({ type: FAILURE, errors: [ 'Connection error' ] }); + .catch((e) => { + logger.error("Error during API request. ", e) + return next({ type: FAILURE, errors: [ "Connection error", String(e) ] }); }); }; diff --git a/frontend/app/assets/index.html b/frontend/app/assets/index.html index b2e0d8dc9..75914f4fb 100644 --- a/frontend/app/assets/index.html +++ b/frontend/app/assets/index.html @@ -1,17 +1,21 @@ - - OpenReplay - - - - - - - - - - -

Loading...

- + + OpenReplay + + + + + + + + + + + + + + +

Loading...

+ diff --git a/frontend/app/assets/integrations/aws.svg b/frontend/app/assets/integrations/aws.svg new file mode 100644 index 000000000..c18fbdab2 --- /dev/null +++ b/frontend/app/assets/integrations/aws.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/app/assets/integrations/bugsnag.svg b/frontend/app/assets/integrations/bugsnag.svg index 26a3a13b8..cc97e195b 100644 --- a/frontend/app/assets/integrations/bugsnag.svg +++ b/frontend/app/assets/integrations/bugsnag.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/frontend/app/assets/integrations/google-cloud.svg b/frontend/app/assets/integrations/google-cloud.svg new file mode 100644 index 000000000..93f614043 --- /dev/null +++ b/frontend/app/assets/integrations/google-cloud.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/app/assets/integrations/newrelic.svg b/frontend/app/assets/integrations/newrelic.svg index cc4aea514..061e7e0a3 100644 --- a/frontend/app/assets/integrations/newrelic.svg +++ b/frontend/app/assets/integrations/newrelic.svg @@ -1 +1,12 @@ -NewRelic-logo-square \ No newline at end of file + + + + + + + + + + + + diff --git a/frontend/app/assets/integrations/rollbar.svg b/frontend/app/assets/integrations/rollbar.svg index 2f6538118..0d183182b 100644 --- a/frontend/app/assets/integrations/rollbar.svg +++ b/frontend/app/assets/integrations/rollbar.svg @@ -1,20 +1,10 @@ - - - - -rollbar-logo-color-vertical - - - - - + + + + + + + + diff --git a/frontend/app/components/Alerts/AlertForm.js b/frontend/app/components/Alerts/AlertForm.js index 1701c8e0a..f5e4ee236 100644 --- a/frontend/app/components/Alerts/AlertForm.js +++ b/frontend/app/components/Alerts/AlertForm.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React, { useEffect } from 'react'; import { Button, Form, Input, SegmentSelection, Checkbox, Message, Link, Icon } from 'UI'; import { alertMetrics as metrics } from 'App/constants'; import { alertConditions as conditions } from 'App/constants'; @@ -9,333 +9,322 @@ import DropdownChips from './DropdownChips'; import { validateEmail } from 'App/validate'; import cn from 'classnames'; import { fetchTriggerOptions } from 'Duck/alerts'; -import Select from 'Shared/Select' +import Select from 'Shared/Select'; const thresholdOptions = [ - { label: '15 minutes', value: 15 }, - { label: '30 minutes', value: 30 }, - { label: '1 hour', value: 60 }, - { label: '2 hours', value: 120 }, - { label: '4 hours', value: 240 }, - { label: '1 day', value: 1440 }, + { label: '15 minutes', value: 15 }, + { label: '30 minutes', value: 30 }, + { label: '1 hour', value: 60 }, + { label: '2 hours', value: 120 }, + { label: '4 hours', value: 240 }, + { label: '1 day', value: 1440 }, ]; const changeOptions = [ - { label: 'change', value: 'change' }, - { label: '% change', value: 'percent' }, + { label: 'change', value: 'change' }, + { label: '% change', value: 'percent' }, ]; -const Circle = ({ text }) => ( -
{text}
-) +const Circle = ({ text }) =>
{text}
; const Section = ({ index, title, description, content }) => ( -
-
- -
- {title} - { description &&
{description}
} -
-
+
+
+ +
+ {title} + {description &&
{description}
} +
+
-
- {content} +
{content}
-
-) +); const integrationsRoute = client(CLIENT_TABS.INTEGRATIONS); -const AlertForm = props => { - const { instance, slackChannels, webhooks, loading, onDelete, deleting, triggerOptions, metricId, style={ width: '580px', height: '100vh' } } = props; - const write = ({ target: { value, name } }) => props.edit({ [ name ]: value }) - const writeOption = (e, { name, value }) => props.edit({ [ name ]: value.value }); - const onChangeCheck = ({ target: { checked, name }}) => props.edit({ [ name ]: checked }) - // const onChangeOption = ({ checked, name }) => props.edit({ [ name ]: checked }) - // const onChangeCheck = (e) => { console.log(e) } +const AlertForm = (props) => { + const { + instance, + slackChannels, + webhooks, + loading, + onDelete, + deleting, + triggerOptions, + metricId, + style = { width: '580px', height: '100vh' }, + } = props; + const write = ({ target: { value, name } }) => props.edit({ [name]: value }); + const writeOption = (e, { name, value }) => props.edit({ [name]: value.value }); + const onChangeCheck = ({ target: { checked, name } }) => props.edit({ [name]: checked }); + // const onChangeOption = ({ checked, name }) => props.edit({ [ name ]: checked }) + // const onChangeCheck = (e) => { console.log(e) } - useEffect(() => { - props.fetchTriggerOptions(); - }, []) + useEffect(() => { + props.fetchTriggerOptions(); + }, []); - const writeQueryOption = (e, { name, value }) => { - const { query } = instance; - props.edit({ query: { ...query, [name] : value } }); - } + const writeQueryOption = (e, { name, value }) => { + const { query } = instance; + props.edit({ query: { ...query, [name]: value } }); + }; - const writeQuery = ({ target: { value, name } }) => { - const { query } = instance; - props.edit({ query: { ...query, [name] : value } }); - } + const writeQuery = ({ target: { value, name } }) => { + const { query } = instance; + props.edit({ query: { ...query, [name]: value } }); + }; - const metric = (instance && instance.query.left) ? triggerOptions.find(i => i.value === instance.query.left) : null; - const unit = metric ? metric.unit : ''; - const isThreshold = instance.detectionMethod === 'threshold'; + const metric = instance && instance.query.left ? triggerOptions.find((i) => i.value === instance.query.left) : null; + const unit = metric ? metric.unit : ''; + const isThreshold = instance.detectionMethod === 'threshold'; - return ( -
props.onSubmit(instance)} id="alert-form"> -
- -
-
- props.edit({ [ name ]: value }) } - value={{ value: instance.detectionMethod }} - list={ [ - { name: 'Threshold', value: 'threshold' }, - { name: 'Change', value: 'change' }, - ]} - /> -
- {isThreshold && 'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.'} - {!isThreshold && 'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'} -
-
+ return ( + props.onSubmit(instance)} id="alert-form"> +
+ +
+
+ props.edit({ [name]: value })} + value={{ value: instance.detectionMethod }} + list={[ + { name: 'Threshold', value: 'threshold' }, + { name: 'Change', value: 'change' }, + ]} + /> +
+ {isThreshold && 'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.'} + {!isThreshold && + 'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'} +
+
+
+ } + /> + +
+ +
+ {!isThreshold && ( +
+ + i.value === instance.query.left)} + // onChange={ writeQueryOption } + onChange={({ value }) => writeQueryOption(null, { name: 'left', value: value.value })} + /> +
+ +
+ +
+ + {'test'} + + )} + {!unit && ( + + )} +
+
+ +
+ + writeOption(null, { name: 'previousPeriod', value })} + /> +
+ )} +
+ } + /> + +
+ +
+
+ + + +
+ + {instance.slack && ( +
+ +
+ props.edit({ slackInput: selected })} + /> +
+
+ )} + + {instance.email && ( +
+ +
+ props.edit({ emailInput: selected })} + /> +
+
+ )} + + {instance.webhook && ( +
+ + props.edit({ webhookInput: selected })} + /> +
+ )} +
+ } + />
- } - /> -
- -
- {!isThreshold && ( -
- - i.value === instance.query.left) } - // onChange={ writeQueryOption } - onChange={ ({ value }) => writeQueryOption(null, { name: 'left', value: value.value }) } - /> -
- -
- -
- - {'test'} - - )} - { !unit && ( - - )} +
+ {instance.exists() && ( + + )}
-
- -
- - writeOption(null, { name: 'previousPeriod', value }) } - /> -
- )}
- } - /> + + ); +}; -
- -
-
- - - -
- - { instance.slack && ( -
- -
- props.edit({ 'slackInput': selected })} - /> -
-
- )} - - {instance.email && ( -
- -
- props.edit({ 'emailInput': selected })} - /> -
-
- )} - - - {instance.webhook && ( -
- - props.edit({ 'webhookInput': selected })} - /> -
- )} -
- } - /> -
- - -
-
- -
- -
-
- {instance.exists() && ( - - )} -
-
- - ) -} - -export default connect(state => ({ - instance: state.getIn(['alerts', 'instance']), - triggerOptions: state.getIn(['alerts', 'triggerOptions']), - loading: state.getIn(['alerts', 'saveRequest', 'loading']), - deleting: state.getIn(['alerts', 'removeRequest', 'loading']) -}), { fetchTriggerOptions })(AlertForm) +export default connect( + (state) => ({ + instance: state.getIn(['alerts', 'instance']), + triggerOptions: state.getIn(['alerts', 'triggerOptions']), + loading: state.getIn(['alerts', 'saveRequest', 'loading']), + deleting: state.getIn(['alerts', 'removeRequest', 'loading']), + }), + { fetchTriggerOptions } +)(AlertForm); diff --git a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx index 8869f3a02..dc4c9db15 100644 --- a/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx +++ b/frontend/app/components/Alerts/AlertFormModal/AlertFormModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react'; import { SlideModal, IconButton } from 'UI'; import { init, edit, save, remove } from 'Duck/alerts'; import { fetchList as fetchWebhooks } from 'Duck/webhook'; @@ -9,93 +9,98 @@ import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule'; import { confirm } from 'UI'; interface Props { - showModal?: boolean; - metricId?: number; - onClose?: () => void; - webhooks: any; - fetchWebhooks: Function; - save: Function; - remove: Function; - init: Function; - edit: Function; + showModal?: boolean; + metricId?: number; + onClose?: () => void; + webhooks: any; + fetchWebhooks: Function; + save: Function; + remove: Function; + init: Function; + edit: Function; } function AlertFormModal(props: Props) { - const { metricId = null, showModal = false, webhooks } = props; - const [showForm, setShowForm] = useState(false); + const { metricId = null, showModal = false, webhooks } = props; + const [showForm, setShowForm] = useState(false); - useEffect(() => { - props.fetchWebhooks(); - }, []) + useEffect(() => { + props.fetchWebhooks(); + }, []); - const slackChannels = webhooks.filter(hook => hook.type === SLACK).map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS(); - const hooks = webhooks.filter(hook => hook.type === WEBHOOK).map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS(); + const slackChannels = webhooks + .filter((hook) => hook.type === SLACK) + .map(({ webhookId, name }) => ({ value: webhookId, text: name })) + .toJS(); + const hooks = webhooks + .filter((hook) => hook.type === WEBHOOK) + .map(({ webhookId, name }) => ({ value: webhookId, text: name })) + .toJS(); - const saveAlert = instance => { - const wasUpdating = instance.exists(); - props.save(instance).then(() => { - if (!wasUpdating) { - toggleForm(null, false); - } - if (props.onClose) { - props.onClose(); - } - }) - } + const saveAlert = (instance) => { + const wasUpdating = instance.exists(); + props.save(instance).then(() => { + if (!wasUpdating) { + toggleForm(null, false); + } + if (props.onClose) { + props.onClose(); + } + }); + }; - const onDelete = async (instance) => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this alert?` - })) { - props.remove(instance.alertId).then(() => { - toggleForm(null, false); - }); - } - } - - const toggleForm = (instance, state) => { - if (instance) { - props.init(instance) - } - return setShowForm(state ? state : !showForm); - } - - return ( - - { 'Create Alert' } - {/* toggleForm({}, true) } - /> */} -
+ const onDelete = async (instance) => { + if ( + await confirm({ + header: 'Confirm', + confirmButton: 'Yes, delete', + confirmation: `Are you sure you want to permanently delete this alert?`, + }) + ) { + props.remove(instance.alertId).then(() => { + toggleForm(null, false); + }); } - isDisplayed={ showModal } - onClose={props.onClose} - size="medium" - content={ showModal && - { + if (instance) { + props.init(instance); + } + return setShowForm(state ? state : !showForm); + }; + + return ( + + {'Create Alert'} +
+ } + isDisplayed={showModal} onClose={props.onClose} - onDelete={onDelete} - style={{ width: '580px', height: '100vh - 200px' }} - /> - } - /> - ); + size="medium" + content={ + showModal && ( + + ) + } + /> + ); } -export default connect(state => ({ - webhooks: state.getIn(['webhooks', 'list']), - instance: state.getIn(['alerts', 'instance']), -}), { init, edit, save, remove, fetchWebhooks, setShowAlerts })(AlertFormModal) \ No newline at end of file +export default connect( + (state) => ({ + webhooks: state.getIn(['webhooks', 'list']), + instance: state.getIn(['alerts', 'instance']), + }), + { init, edit, save, remove, fetchWebhooks, setShowAlerts } +)(AlertFormModal); diff --git a/frontend/app/components/Alerts/Alerts.js b/frontend/app/components/Alerts/Alerts.js index b24665a68..ed825abaf 100644 --- a/frontend/app/components/Alerts/Alerts.js +++ b/frontend/app/components/Alerts/Alerts.js @@ -10,95 +10,100 @@ import { setShowAlerts } from 'Duck/dashboard'; import { EMAIL, SLACK, WEBHOOK } from 'App/constants/schedule'; import { confirm } from 'UI'; -const Alerts = props => { - const { webhooks, setShowAlerts } = props; - const [showForm, setShowForm] = useState(false); +const Alerts = (props) => { + const { webhooks, setShowAlerts } = props; + const [showForm, setShowForm] = useState(false); - useEffect(() => { - props.fetchWebhooks(); - }, []) + useEffect(() => { + props.fetchWebhooks(); + }, []); - const slackChannels = webhooks.filter(hook => hook.type === SLACK).map(({ webhookId, name }) => ({ value: webhookId, label: name })).toJS(); - const hooks = webhooks.filter(hook => hook.type === WEBHOOK).map(({ webhookId, name }) => ({ value: webhookId, label: name })).toJS(); + const slackChannels = webhooks + .filter((hook) => hook.type === SLACK) + .map(({ webhookId, name }) => ({ value: webhookId, label: name })) + .toJS(); + const hooks = webhooks + .filter((hook) => hook.type === WEBHOOK) + .map(({ webhookId, name }) => ({ value: webhookId, label: name })) + .toJS(); - const saveAlert = instance => { - const wasUpdating = instance.exists(); - props.save(instance).then(() => { - if (!wasUpdating) { - toast.success('New alert saved') - toggleForm(null, false); - } else { - toast.success('Alert updated') - } - }) - } + const saveAlert = (instance) => { + const wasUpdating = instance.exists(); + props.save(instance).then(() => { + if (!wasUpdating) { + toast.success('New alert saved'); + toggleForm(null, false); + } else { + toast.success('Alert updated'); + } + }); + }; - const onDelete = async (instance) => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this alert?` - })) { - props.remove(instance.alertId).then(() => { - toggleForm(null, false); - }); - } - } + const onDelete = async (instance) => { + if ( + await confirm({ + header: 'Confirm', + confirmButton: 'Yes, delete', + confirmation: `Are you sure you want to permanently delete this alert?`, + }) + ) { + props.remove(instance.alertId).then(() => { + toggleForm(null, false); + }); + } + }; - const toggleForm = (instance, state) => { - if (instance) { - props.init(instance) - } - return setShowForm(state ? state : !showForm); - } + const toggleForm = (instance, state) => { + if (instance) { + props.init(instance); + } + return setShowForm(state ? state : !showForm); + }; - return ( -
- - { 'Alerts' } - toggleForm({}, true) } + return ( +
+ + {'Alerts'} + toggleForm({}, true)} /> +
+ } + isDisplayed={true} + onClose={() => { + toggleForm({}, false); + setShowAlerts(false); + }} + size="small" + content={ + { + toggleForm(alert, true); + }} + onClickCreate={() => toggleForm({}, true)} + /> + } + detailContent={ + showForm && ( + toggleForm({}, false)} + onDelete={onDelete} + /> + ) + } /> -
- } - isDisplayed={ true } - onClose={ () => { - toggleForm({}, false); - setShowAlerts(false); - } } - size="small" - content={ - { - toggleForm(alert, true) - }} - /> - } - detailContent={ - showForm && ( - toggleForm({}, false) } - onDelete={onDelete} - /> - ) - } - /> - - ) -} + + ); +}; -export default connect(state => ({ - webhooks: state.getIn(['webhooks', 'list']), - instance: state.getIn(['alerts', 'instance']), -}), { init, edit, save, remove, fetchWebhooks, setShowAlerts })(Alerts) +export default connect( + (state) => ({ + webhooks: state.getIn(['webhooks', 'list']), + instance: state.getIn(['alerts', 'instance']), + }), + { init, edit, save, remove, fetchWebhooks, setShowAlerts } +)(Alerts); diff --git a/frontend/app/components/Alerts/AlertsList.js b/frontend/app/components/Alerts/AlertsList.js index 21ea6448d..5a874e0fa 100644 --- a/frontend/app/components/Alerts/AlertsList.js +++ b/frontend/app/components/Alerts/AlertsList.js @@ -1,55 +1,58 @@ -import React, { useEffect, useState } from 'react' -import { Loader, NoContent, Input } from 'UI'; -import AlertItem from './AlertItem' +import React, { useEffect, useState } from 'react'; +import { Loader, NoContent, Input, Button } from 'UI'; +import AlertItem from './AlertItem'; import { fetchList, init } from 'Duck/alerts'; import { connect } from 'react-redux'; import { getRE } from 'App/utils'; -const AlertsList = props => { - const { loading, list, instance, onEdit } = props; - const [query, setQuery] = useState('') - - useEffect(() => { - props.fetchList() - }, []) +const AlertsList = (props) => { + const { loading, list, instance, onEdit } = props; + const [query, setQuery] = useState(''); - const filterRE = getRE(query, 'i'); - const _filteredList = list.filter(({ name, query: { left } }) => filterRE.test(name) || filterRE.test(left)); + useEffect(() => { + props.fetchList(); + }, []); - return ( -
-
- setQuery(value)} - /> -
- - -
- {_filteredList.map(a => ( -
- onEdit(a.toData())} - /> -
- ))} -
-
-
-
- ) -} + const filterRE = getRE(query, 'i'); + const _filteredList = list.filter(({ name, query: { left } }) => filterRE.test(name) || filterRE.test(left)); -export default connect(state => ({ - list: state.getIn(['alerts', 'list']).sort((a, b ) => b.createdAt - a.createdAt), - instance: state.getIn(['alerts', 'instance']), - loading: state.getIn(['alerts', 'loading']) -}), { fetchList, init })(AlertsList) + return ( +
+
+ setQuery(value)} /> +
+ + +
Alerts helps your team stay up to date with the activity on your app.
+ +
+ } + size="small" + show={list.size === 0} + > +
+ {_filteredList.map((a) => ( +
+ onEdit(a.toData())} /> +
+ ))} +
+ + + + ); +}; + +export default connect( + (state) => ({ + list: state.getIn(['alerts', 'list']).sort((a, b) => b.createdAt - a.createdAt), + instance: state.getIn(['alerts', 'instance']), + loading: state.getIn(['alerts', 'loading']), + }), + { fetchList, init } +)(AlertsList); diff --git a/frontend/app/components/Alerts/DropdownChips/DropdownChips.js b/frontend/app/components/Alerts/DropdownChips/DropdownChips.js index 7a7e81ada..1f805057d 100644 --- a/frontend/app/components/Alerts/DropdownChips/DropdownChips.js +++ b/frontend/app/components/Alerts/DropdownChips/DropdownChips.js @@ -1,79 +1,66 @@ -import React from 'react' +import React from 'react'; import { Input, TagBadge } from 'UI'; import Select from 'Shared/Select'; -const DropdownChips = ({ - textFiled = false, - validate = null, - placeholder = '', - selected = [], - options = [], - badgeClassName = 'lowercase', - onChange = () => null, - ...props +const DropdownChips = ({ + textFiled = false, + validate = null, + placeholder = '', + selected = [], + options = [], + badgeClassName = 'lowercase', + onChange = () => null, + ...props }) => { - const onRemove = id => { - onChange(selected.filter(i => i !== id)) - } + const onRemove = (id) => { + onChange(selected.filter((i) => i !== id)); + }; - const onSelect = ({ value }) => { - const newSlected = selected.concat(value.value); - onChange(newSlected) - }; + const onSelect = ({ value }) => { + const newSlected = selected.concat(value.value); + onChange(newSlected); + }; - const onKeyPress = e => { - const val = e.target.value; - if (e.key !== 'Enter' || selected.includes(val)) return; - e.preventDefault(); - e.stopPropagation(); - if (validate && !validate(val)) return; + const onKeyPress = (e) => { + const val = e.target.value; + if (e.key !== 'Enter' || selected.includes(val)) return; + e.preventDefault(); + e.stopPropagation(); + if (validate && !validate(val)) return; - const newSlected = selected.concat(val); - e.target.value = ''; - onChange(newSlected); - } + const newSlected = selected.concat(val); + e.target.value = ''; + onChange(newSlected); + }; - const _options = options.filter(item => !selected.includes(item.value)) + const _options = options.filter((item) => !selected.includes(item.value)); + + const renderBadge = (item) => { + const val = typeof item === 'string' ? item : item.value; + const text = typeof item === 'string' ? item : item.label; + return onRemove(val)} outline={true} />; + }; - const renderBadge = item => { - const val = typeof item === 'string' ? item : item.value; - const text = typeof item === 'string' ? item : item.label; return ( - onRemove(val) } - outline={ true } - /> - ) - } +
+ {textFiled ? ( + + ) : ( + - ) : ( - { this.focusElement = ref; } } - name="key" - value={ field.key } - onChange={ this.write } - placeholder="Field Name" - /> - + render() { + const { field, errors } = this.props; + const exists = field.exists(); + return ( +
+

{exists ? 'Update' : 'Add'} Metadata Field

+
+ + + { + this.focusElement = ref; + }} + name="key" + value={field.key} + onChange={this.write} + placeholder="Field Name" + /> + - { errors && -
- { errors.map(error => { error }) } -
- } + {errors && ( +
+ {errors.map((error) => ( + + {error} + + ))} +
+ )} - - -
- ); - } +
+
+ + +
+ + +
+ +
+ ); + } } export default CustomFieldForm; diff --git a/frontend/app/components/Client/CustomFields/CustomFields.js b/frontend/app/components/Client/CustomFields/CustomFields.js index 4c3d0bbc8..d5abbb2c6 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.js +++ b/frontend/app/components/Client/CustomFields/CustomFields.js @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import cn from 'classnames'; import { connect } from 'react-redux'; import withPageTitle from 'HOCs/withPageTitle'; -import { IconButton, SlideModal, Loader, NoContent, Icon, TextLink } from 'UI'; +import { Button, Loader, NoContent, TextLink } from 'UI'; import { init, fetchList, save, remove } from 'Duck/customField'; import SiteDropdown from 'Shared/SiteDropdown'; import styles from './customFields.module.css'; @@ -10,121 +10,118 @@ import CustomFieldForm from './CustomFieldForm'; import ListItem from './ListItem'; import { confirm } from 'UI'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import { useModal } from 'App/components/Modal'; -@connect(state => ({ - fields: state.getIn(['customFields', 'list']).sortBy(i => i.index), - field: state.getIn(['customFields', 'instance']), - loading: state.getIn(['customFields', 'fetchRequest', 'loading']), - sites: state.getIn([ 'site', 'list' ]), - errors: state.getIn([ 'customFields', 'saveRequest', 'errors' ]), -}), { - init, - fetchList, - save, - remove, -}) -@withPageTitle('Metadata - OpenReplay Preferences') -class CustomFields extends React.Component { - state = { showModal: false, currentSite: this.props.sites.get(0), deletingItem: null }; +function CustomFields(props) { + const [currentSite, setCurrentSite] = React.useState(props.sites.get(0)); + const [deletingItem, setDeletingItem] = React.useState(null); + const { showModal, hideModal } = useModal(); - componentWillMount() { - const activeSite = this.props.sites.get(0); - if (!activeSite) return; - - this.props.fetchList(activeSite.id); - } + useEffect(() => { + const activeSite = props.sites.get(0); + if (!activeSite) return; - save = (field) => { - const { currentSite } = this.state; - this.props.save(currentSite.id, field).then(() => { - const { errors } = this.props; - if (!errors || errors.size === 0) { - return this.closeModal(); - } - }); - }; + props.fetchList(activeSite.id); + }, []); - closeModal = () => this.setState({ showModal: false }); - init = (field) => { - this.props.init(field); - this.setState({ showModal: true }); - } - - onChangeSelect = ({ value }) => { - const site = this.props.sites.find(s => s.id === value.value); - this.setState({ currentSite: site }) - this.props.fetchList(site.id); - } - - removeMetadata = async (field) => { - if (await confirm({ - header: 'Metadata', - confirmation: `Are you sure you want to remove?` - })) { - const { currentSite } = this.state; - this.setState({ deletingItem: field.index }); - this.props.remove(currentSite.id, field.index) - .then(() => this.setState({ deletingItem: null })); - } - } - - render() { - const { fields, field, loading } = this.props; - const { showModal, currentSite, deletingItem } = this.state; - return ( -
- } - onClose={ this.closeModal } - /> -
-

{ 'Metadata' }

-
- -
- this.init() } /> - -
- - - - -
No data available.
-
+ const save = (field) => { + props.save(currentSite.id, field).then(() => { + const { errors } = props; + if (!errors || errors.size === 0) { + hideModal(); } - size="small" - show={ fields.size === 0 } - // animatedIcon="empty-state" - > -
- { fields.filter(i => i.index).map(field => ( - this.removeMetadata(field) } + }); + }; + + const init = (field) => { + props.init(field); + showModal( removeMetadata(field)} />); + }; + + const onChangeSelect = ({ value }) => { + const site = props.sites.find((s) => s.id === value.value); + setCurrentSite(site); + props.fetchList(site.id); + }; + + const removeMetadata = async (field) => { + if ( + await confirm({ + header: 'Metadata', + confirmation: `Are you sure you want to remove?`, + }) + ) { + setDeletingItem(field.index); + props + .remove(currentSite.id, field.index) + .then(() => { + hideModal(); + }) + .finally(() => { + setDeletingItem(null); + }); + } + }; + + const { fields, loading } = props; + return ( +
+
+

{'Metadata'}

+
+ +
+
- - -
+ + + + +
No data available.
+
+ } + size="small" + show={fields.size === 0} + > +
+ {fields + .filter((i) => i.index) + .map((field) => ( + removeMetadata(field) } + /> + ))} +
+ + +
); - } } -export default CustomFields; +export default connect( + (state) => ({ + fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index), + field: state.getIn(['customFields', 'instance']), + loading: state.getIn(['customFields', 'fetchRequest', 'loading']), + sites: state.getIn(['site', 'list']), + errors: state.getIn(['customFields', 'saveRequest', 'errors']), + }), + { + init, + fetchList, + save, + remove, + } +)(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields)); diff --git a/frontend/app/components/Client/CustomFields/ListItem.js b/frontend/app/components/Client/CustomFields/ListItem.js index ef806fc93..19c49e925 100644 --- a/frontend/app/components/Client/CustomFields/ListItem.js +++ b/frontend/app/components/Client/CustomFields/ListItem.js @@ -1,22 +1,26 @@ import React from 'react'; -import cn from 'classnames' -import { Icon } from 'UI'; +import cn from 'classnames'; +import { Button } from 'UI'; import styles from './listItem.module.css'; -const ListItem = ({ field, onEdit, onDelete, disabled }) => { - return ( -
field.index != 0 && onEdit(field) } > - { field.key } -
-
{ e.stopPropagation(); onDelete(field) } }> - +const ListItem = ({ field, onEdit, disabled }) => { + return ( +
field.index != 0 && onEdit(field)} + > + {field.key} +
+
-
- -
-
-
- ); + ); }; export default ListItem; diff --git a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js index 4cf2d0e7f..83319959a 100644 --- a/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js +++ b/frontend/app/components/Client/Integrations/AssistDoc/AssistDoc.js @@ -1,59 +1,56 @@ import React from 'react'; -import Highlight from 'react-highlight' +import Highlight from 'react-highlight'; import DocLink from 'Shared/DocLink/DocLink'; -import AssistScript from './AssistScript' -import AssistNpm from './AssistNpm' +import AssistScript from './AssistScript'; +import AssistNpm from './AssistNpm'; import { Tabs } from 'UI'; import { useState } from 'react'; -const NPM = 'NPM' -const SCRIPT = 'SCRIPT' +const NPM = 'NPM'; +const SCRIPT = 'SCRIPT'; const TABS = [ - { key: SCRIPT, text: SCRIPT }, - { key: NPM, text: NPM }, -] + { key: SCRIPT, text: SCRIPT }, + { key: NPM, text: NPM }, +]; const AssistDoc = (props) => { - const { projectKey } = props; - const [activeTab, setActiveTab] = useState(SCRIPT) - + const { projectKey } = props; + const [activeTab, setActiveTab] = useState(SCRIPT); - const renderActiveTab = () => { - switch (activeTab) { - case SCRIPT: - return - case NPM: - return - } - return null; - } + const renderActiveTab = () => { + switch (activeTab) { + case SCRIPT: + return ; + case NPM: + return ; + } + return null; + }; + return ( +
+

Assist

+
+
+ OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them + without requiring any 3rd-party screen sharing software. +
- return ( -
-
OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.
+
Installation
+ {`npm i @openreplay/tracker-assist`} +
-
Installation
- - {`npm i @openreplay/tracker-assist`} - -
+
Usage
+ setActiveTab(tab)} /> -
Usage
- setActiveTab(tab) } - /> +
{renderActiveTab()}
-
- { renderActiveTab() } -
- - -
- ) + +
+
+ ); }; -AssistDoc.displayName = "AssistDoc"; +AssistDoc.displayName = 'AssistDoc'; export default AssistDoc; diff --git a/frontend/app/components/Client/Integrations/AxiosDoc/AxiosDoc.js b/frontend/app/components/Client/Integrations/AxiosDoc/AxiosDoc.js index 8fe32cfd0..9b624bbe4 100644 --- a/frontend/app/components/Client/Integrations/AxiosDoc/AxiosDoc.js +++ b/frontend/app/components/Client/Integrations/AxiosDoc/AxiosDoc.js @@ -1,40 +1,46 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const AxiosDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture axios requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-axios`} - - -
Usage
-

Initialize the @openreplay/tracker package as usual then load the axios plugin. Note that OpenReplay axios plugin requires axios@^0.21.2 as a peer dependency.

-
+ const { projectKey } = props; + return ( +
+

Axios

+
+
+ This plugin allows you to capture axios requests and inspect them later on while replaying session recordings. This is very useful + for understanding and fixing issues. +
-
Usage
- - {`import tracker from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-axios`} + +
Usage
+

+ Initialize the @openreplay/tracker package as usual then load the axios plugin. Note that OpenReplay axios plugin requires + axios@^0.21.2 as a peer dependency. +

+
+ +
Usage
+ + {`import tracker from '@openreplay/tracker'; import trackerAxios from '@openreplay/tracker-axios'; const tracker = new OpenReplay({ projectKey: '${projectKey}' }); tracker.use(trackerAxios(options)); // check list of available options below tracker.start();`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerAxios from '@openreplay/tracker-axios/cjs'; const tracker = new OpenReplay({ projectKey: '${projectKey}' @@ -47,15 +53,16 @@ function MyApp() { }, []) //... }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -AxiosDoc.displayName = "AxiosDoc"; +AxiosDoc.displayName = 'AxiosDoc'; export default AxiosDoc; diff --git a/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js b/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js index b1aba5a30..15d8ddef1 100644 --- a/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js +++ b/frontend/app/components/Client/Integrations/BugsnagForm/BugsnagForm.js @@ -1,32 +1,35 @@ import React from 'react'; import { tokenRE } from 'Types/integrations/bugsnagConfig'; -import IntegrationForm from '../IntegrationForm'; +import IntegrationForm from '../IntegrationForm'; import ProjectListDropdown from './ProjectListDropdown'; import DocLink from 'Shared/DocLink/DocLink'; const BugsnagForm = (props) => ( - <> -
-
How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.
- +
+

Bugsnag

+
+
How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.
+ +
+ tokenRE.test(config.authorizationToken), + component: ProjectListDropdown, + }, + ]} + />
- tokenRE.test(config.authorizationToken), - component: ProjectListDropdown, - } - ]} - /> - ); -BugsnagForm.displayName = "BugsnagForm"; +BugsnagForm.displayName = 'BugsnagForm'; -export default BugsnagForm; \ No newline at end of file +export default BugsnagForm; diff --git a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js index 482167c72..bd9604b01 100644 --- a/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js +++ b/frontend/app/components/Client/Integrations/CloudwatchForm/CloudwatchForm.js @@ -1,43 +1,48 @@ import React from 'react'; import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig'; -import IntegrationForm from '../IntegrationForm'; +import IntegrationForm from '../IntegrationForm'; import LogGroupDropdown from './LogGroupDropdown'; import RegionDropdown from './RegionDropdown'; import DocLink from 'Shared/DocLink/DocLink'; const CloudwatchForm = (props) => ( - <> -
-
How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.
- +
+

Cloud Watch

+
+
How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.
+ +
+ + config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH && + config.region !== '' && + config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH, + }, + ]} + />
- - config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH && - config.region !== '' && - config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH - } - ]} - /> - ); -CloudwatchForm.displayName = "CloudwatchForm"; +CloudwatchForm.displayName = 'CloudwatchForm'; -export default CloudwatchForm; \ No newline at end of file +export default CloudwatchForm; diff --git a/frontend/app/components/Client/Integrations/DatadogForm.js b/frontend/app/components/Client/Integrations/DatadogForm.js index 76ca0734d..46360259c 100644 --- a/frontend/app/components/Client/Integrations/DatadogForm.js +++ b/frontend/app/components/Client/Integrations/DatadogForm.js @@ -1,29 +1,32 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const DatadogForm = (props) => ( - <> -
-
How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.
- +
+

Datadog

+
+
How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -DatadogForm.displayName = "DatadogForm"; +DatadogForm.displayName = 'DatadogForm'; export default DatadogForm; diff --git a/frontend/app/components/Client/Integrations/ElasticsearchForm.js b/frontend/app/components/Client/Integrations/ElasticsearchForm.js index 271ccefe1..ad33b6302 100644 --- a/frontend/app/components/Client/Integrations/ElasticsearchForm.js +++ b/frontend/app/components/Client/Integrations/ElasticsearchForm.js @@ -1,75 +1,88 @@ import React from 'react'; import { connect } from 'react-redux'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import { withRequest } from 'HOCs'; import { edit } from 'Duck/integrations/actions'; import DocLink from 'Shared/DocLink/DocLink'; -@connect(state => ({ - config: state.getIn([ 'elasticsearch', 'instance' ]) -}), { edit }) +@connect( + (state) => ({ + config: state.getIn(['elasticsearch', 'instance']), + }), + { edit } +) @withRequest({ - dataName: "isValid", - initialData: false, - dataWrapper: data => data.state, - requestName: "validateConfig", - endpoint: '/integrations/elasticsearch/test', - method: 'POST', + dataName: 'isValid', + initialData: false, + dataWrapper: (data) => data.state, + requestName: 'validateConfig', + endpoint: '/integrations/elasticsearch/test', + method: 'POST', }) export default class ElasticsearchForm extends React.PureComponent { - componentWillReceiveProps(newProps) { - const { config: { host, port, apiKeyId, apiKey } } = this.props; - const { loading, config } = newProps; - const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey; - if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) { - this.validateConfig(newProps); + componentWillReceiveProps(newProps) { + const { + config: { host, port, apiKeyId, apiKey }, + } = this.props; + const { loading, config } = newProps; + const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey; + if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) { + this.validateConfig(newProps); + } } - } - validateConfig = (newProps) => { - const { config } = newProps; - this.props.validateConfig({ - host: config.host, - port: config.port, - apiKeyId: config.apiKeyId, - apiKey: config.apiKey, - }).then((res) => { - const { isValid } = this.props; - this.props.edit('elasticsearch', { isValid: isValid }) - }); - } + validateConfig = (newProps) => { + const { config } = newProps; + this.props + .validateConfig({ + host: config.host, + port: config.port, + apiKeyId: config.apiKeyId, + apiKey: config.apiKey, + }) + .then((res) => { + const { isValid } = this.props; + this.props.edit('elasticsearch', { isValid: isValid }); + }); + }; - render() { - const props = this.props; - return ( - <> -
-
How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.
- -
- - - ) - } -}; + render() { + const props = this.props; + return ( +
+

Elasticsearch

+
+
How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.
+ +
+ +
+ ); + } +} diff --git a/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js b/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js index 8d9bbd5b9..b4b8b537d 100644 --- a/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js +++ b/frontend/app/components/Client/Integrations/FetchDoc/FetchDoc.js @@ -1,29 +1,32 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const FetchDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-fetch --save`} - - -
Usage
-

Use the provided fetch method from the plugin instead of the one built-in.

-
+ const { projectKey } = props; + return ( +
+

Fetch

+
+
+ This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful + for understanding and fixing issues. +
-
Usage
- - {`import tracker from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-fetch --save`} + +
Usage
+

Use the provided fetch method from the plugin instead of the one built-in.

+
+ +
Usage
+ + {`import tracker from '@openreplay/tracker'; import trackerFetch from '@openreplay/tracker-fetch'; //... const tracker = new OpenReplay({ @@ -34,11 +37,11 @@ tracker.start(); export const fetch = tracker.use(trackerFetch()); // check list of available options below //... fetch('https://api.openreplay.com/').then(response => console.log(response.json()));`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerFetch from '@openreplay/tracker-fetch/cjs'; //... const tracker = new OpenReplay({ @@ -54,15 +57,16 @@ export const fetch = tracker.use(trackerFetch()); // check list of avai //... fetch('https://api.openreplay.com/').then(response => console.log(response.json())); }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -FetchDoc.displayName = "FetchDoc"; +FetchDoc.displayName = 'FetchDoc'; export default FetchDoc; diff --git a/frontend/app/components/Client/Integrations/GithubForm.js b/frontend/app/components/Client/Integrations/GithubForm.js index 586ab3093..7d140732b 100644 --- a/frontend/app/components/Client/Integrations/GithubForm.js +++ b/frontend/app/components/Client/Integrations/GithubForm.js @@ -1,30 +1,31 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const GithubForm = (props) => ( - <> -
-
Integrate GitHub with OpenReplay and create issues directly from the recording page.
-
- -
+
+

Github

+
+
Integrate GitHub with OpenReplay and create issues directly from the recording page.
+
+ +
+
+
- - ); -GithubForm.displayName = "GithubForm"; +GithubForm.displayName = 'GithubForm'; -export default GithubForm; \ No newline at end of file +export default GithubForm; diff --git a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js index a9150bc44..36e883f25 100644 --- a/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js +++ b/frontend/app/components/Client/Integrations/GraphQLDoc/GraphQLDoc.js @@ -1,30 +1,36 @@ import React from 'react'; -import Highlight from 'react-highlight' +import Highlight from 'react-highlight'; import DocLink from 'Shared/DocLink/DocLink'; import ToggleContent from 'Shared/ToggleContent'; const GraphQLDoc = (props) => { - const { projectKey } = props; - return ( -
-

This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.

-

GraphQL plugin is compatible with Apollo and Relay implementations.

- -
Installation
- - {`npm i @openreplay/tracker-graphql --save`} - - -
Usage
-

The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It returns result without changes.

- -
+ const { projectKey } = props; + return ( +
+

GraphQL

+
+

+ This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very + useful for understanding and fixing issues. +

+

GraphQL plugin is compatible with Apollo and Relay implementations.

- - {`import OpenReplay from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-graphql --save`} + +
Usage
+

+ The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It + returns result without changes. +

+ +
+ + + {`import OpenReplay from '@openreplay/tracker'; import trackerGraphQL from '@openreplay/tracker-graphql'; //... const tracker = new OpenReplay({ @@ -33,11 +39,11 @@ const tracker = new OpenReplay({ tracker.start(); //... export const recordGraphQL = tracker.use(trackerGraphQL());`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerGraphQL from '@openreplay/tracker-graphql/cjs'; //... const tracker = new OpenReplay({ @@ -51,15 +57,16 @@ function SomeFunctionalComponent() { } //... export const recordGraphQL = tracker.use(trackerGraphQL());`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -GraphQLDoc.displayName = "GraphQLDoc"; +GraphQLDoc.displayName = 'GraphQLDoc'; export default GraphQLDoc; diff --git a/frontend/app/components/Client/Integrations/IntegrationForm.js b/frontend/app/components/Client/Integrations/IntegrationForm.js index a26576fc3..ad6689f3b 100644 --- a/frontend/app/components/Client/Integrations/IntegrationForm.js +++ b/frontend/app/components/Client/Integrations/IntegrationForm.js @@ -1,144 +1,147 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Input, Form, Button, Checkbox } from 'UI'; +import { Input, Form, Button, Checkbox, Loader } from 'UI'; import SiteDropdown from 'Shared/SiteDropdown'; import { save, init, edit, remove, fetchList } from 'Duck/integrations/actions'; +import { fetchIntegrationList } from 'Duck/integrations/integrations'; -@connect((state, { name, customPath }) => ({ - sites: state.getIn([ 'site', 'list' ]), - initialSiteId: state.getIn([ 'site', 'siteId' ]), - list: state.getIn([ name, 'list' ]), - config: state.getIn([ name, 'instance']), - saving: state.getIn([ customPath || name, 'saveRequest', 'loading']), - removing: state.getIn([ name, 'removeRequest', 'loading']), -}), { - save, - init, - edit, - remove, - fetchList -}) +@connect( + (state, { name, customPath }) => ({ + sites: state.getIn(['site', 'list']), + initialSiteId: state.getIn(['site', 'siteId']), + list: state.getIn([name, 'list']), + config: state.getIn([name, 'instance']), + loading: state.getIn([name, 'fetchRequest', 'loading']), + saving: state.getIn([customPath || name, 'saveRequest', 'loading']), + removing: state.getIn([name, 'removeRequest', 'loading']), + siteId: state.getIn(['integrations', 'siteId']), + }), + { + save, + init, + edit, + remove, + fetchList, + fetchIntegrationList, + } +) export default class IntegrationForm extends React.PureComponent { - constructor(props) { - super(props); - const currentSiteId = this.props.initialSiteId; - this.state = { currentSiteId }; - this.init(currentSiteId); - } - - write = ({ target: { value, name: key, type, checked } }) => { - if (type === 'checkbox') - this.props.edit(this.props.name, { [ key ]: checked }) - else - this.props.edit(this.props.name, { [ key ]: value }) - }; + constructor(props) { + super(props); + // const currentSiteId = this.props.initialSiteId; + // this.state = { currentSiteId }; + // this.init(currentSiteId); + } - onChangeSelect = ({ value }) => { - const { sites, list, name } = this.props; - const site = sites.find(s => s.id === value); - this.setState({ currentSiteId: site.id }) - this.init(value); - } + write = ({ target: { value, name: key, type, checked } }) => { + if (type === 'checkbox') this.props.edit(this.props.name, { [key]: checked }); + else this.props.edit(this.props.name, { [key]: value }); + }; - init = (siteId) => { - const { list, name } = this.props; - const config = (parseInt(siteId) > 0) ? list.find(s => s.projectId === siteId) : undefined; - this.props.init(name, config ? config : list.first()); - } + // onChangeSelect = ({ value }) => { + // const { sites, list, name } = this.props; + // const site = sites.find((s) => s.id === value.value); + // this.setState({ currentSiteId: site.id }); + // this.init(value.value); + // }; - save = () => { - const { config, name, customPath } = this.props; - const isExists = config.exists(); - const { currentSiteId } = this.state; - const { ignoreProject } = this.props; - this.props.save(customPath || name, (!ignoreProject ? currentSiteId : null), config) - .then(() => { - this.props.fetchList(name) - this.props.onClose(); - if (isExists) return; - }); - } + // init = (siteId) => { + // const { list, name } = this.props; + // const config = parseInt(siteId) > 0 ? list.find((s) => s.projectId === siteId) : undefined; + // this.props.init(name, config ? config : list.first()); + // }; - remove = () => { - const { name, config, ignoreProject } = this.props; - this.props.remove(name, !ignoreProject ? config.projectId : null).then(function() { - this.props.onClose(); - this.props.fetchList(name) - }.bind(this)); - } + save = () => { + const { config, name, customPath, ignoreProject } = this.props; + const isExists = config.exists(); + // const { currentSiteId } = this.state; + this.props.save(customPath || name, !ignoreProject ? this.props.siteId : null, config).then(() => { + // this.props.fetchList(name); + this.props.onClose(); + if (isExists) return; + }); + }; - render() { - const { config, saving, removing, formFields, name, loading, ignoreProject } = this.props; - const { currentSiteId } = this.state; + remove = () => { + const { name, config, ignoreProject } = this.props; + this.props.remove(name, !ignoreProject ? config.projectId : null).then( + function () { + this.props.onClose(); + this.props.fetchList(name); + }.bind(this) + ); + }; - return ( -
-
- {!ignoreProject && - - - - - } + render() { + const { config, saving, removing, formFields, name, loading, ignoreProject } = this.props; + // const { currentSiteId } = this.state; - { formFields.map(({ - key, - label, - placeholder=label, - component: Component = 'input', - type = "text", - checkIfDisplayed, - autoFocus=false - }) => (typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) && - ((type === 'checkbox') ? - - - - : - - - - - ) - )} - - + return ( + +
+ + {/* {!ignoreProject && ( + + + + + )} */} - {config.exists() && ( - - )} - -
- ); - } + {formFields.map( + ({ + key, + label, + placeholder = label, + component: Component = 'input', + type = 'text', + checkIfDisplayed, + autoFocus = false, + }) => + (typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) && + (type === 'checkbox' ? ( + + + + ) : ( + + + + + )) + )} + + + + {config.exists() && ( + + )} + +
+ + ); + } } diff --git a/frontend/app/components/Client/Integrations/IntegrationItem.js b/frontend/app/components/Client/Integrations/IntegrationItem.js deleted file mode 100644 index b0bfa258a..000000000 --- a/frontend/app/components/Client/Integrations/IntegrationItem.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import { Icon } from 'UI'; -import stl from './integrationItem.module.css'; - -const onDocLinkClick = (e, link) => { - e.stopPropagation(); - window.open(link, '_blank'); -} - -const IntegrationItem = ({ - deleteHandler = null, icon, url = null, title = '', description = '', onClick = null, dockLink = '', integrated = false -}) => { - return ( -
onClick(e, url) }> - {integrated && ( -
- -
- )} - integration -

{ title }

-
- ) -}; - -export default IntegrationItem; diff --git a/frontend/app/components/Client/Integrations/IntegrationItem.tsx b/frontend/app/components/Client/Integrations/IntegrationItem.tsx new file mode 100644 index 000000000..f1b69c029 --- /dev/null +++ b/frontend/app/components/Client/Integrations/IntegrationItem.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import cn from 'classnames'; +import { Icon, Popup } from 'UI'; +import stl from './integrationItem.module.css'; +import { connect } from 'react-redux'; + +interface Props { + integration: any; + onClick?: (e: React.MouseEvent) => void; + integrated?: boolean; + hide?: boolean; +} + +const IntegrationItem = (props: Props) => { + const { integration, integrated, hide = false } = props; + return hide ? <> : ( +
props.onClick(e)}> + {integrated && ( +
+ + + +
+ )} + integration +
+

{integration.title}

+ {/*

{integration.subtitle && integration.subtitle}

*/} +
+
+ ); +}; + +export default connect((state: any, props: Props) => { + const list = state.getIn([props.integration.slug, 'list']) || []; + return { + // integrated: props.integration.slug === 'issues' ? !!(list.first() && list.first().token) : list.size > 0, + }; +})(IntegrationItem); diff --git a/frontend/app/components/Client/Integrations/Integrations.js b/frontend/app/components/Client/Integrations/Integrations.js_ similarity index 100% rename from frontend/app/components/Client/Integrations/Integrations.js rename to frontend/app/components/Client/Integrations/Integrations.js_ diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx new file mode 100644 index 000000000..33d0520d4 --- /dev/null +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -0,0 +1,173 @@ +import { useModal } from 'App/components/Modal'; +import React, { useEffect } from 'react'; +import BugsnagForm from './BugsnagForm'; +import CloudwatchForm from './CloudwatchForm'; +import DatadogForm from './DatadogForm'; +import ElasticsearchForm from './ElasticsearchForm'; +import GithubForm from './GithubForm'; +import IntegrationItem from './IntegrationItem'; +import JiraForm from './JiraForm'; +import NewrelicForm from './NewrelicForm'; +import RollbarForm from './RollbarForm'; +import SentryForm from './SentryForm'; +import SlackForm from './SlackForm'; +import StackdriverForm from './StackdriverForm'; +import SumoLogicForm from './SumoLogicForm'; +import { fetch, init } from 'Duck/integrations/actions'; +import { fetchIntegrationList, setSiteId } from 'Duck/integrations/integrations'; +import { connect } from 'react-redux'; +import SiteDropdown from 'Shared/SiteDropdown'; +import ReduxDoc from './ReduxDoc'; +import VueDoc from './VueDoc'; +import GraphQLDoc from './GraphQLDoc'; +import NgRxDoc from './NgRxDoc'; +import MobxDoc from './MobxDoc'; +import FetchDoc from './FetchDoc'; +import ProfilerDoc from './ProfilerDoc'; +import AxiosDoc from './AxiosDoc'; +import AssistDoc from './AssistDoc'; +import { PageTitle, Loader } from 'UI'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; + +interface Props { + fetch: (name: string, siteId: string) => void; + init: () => void; + fetchIntegrationList: (siteId: any) => void; + integratedList: any; + initialSiteId: string; + setSiteId: (siteId: string) => void; + siteId: string; + hideHeader?: boolean; + loading?: boolean; +} +function Integrations(props: Props) { + const { initialSiteId, hideHeader = false, loading = false } = props; + const { showModal } = useModal(); + const [integratedList, setIntegratedList] = React.useState([]); + + useEffect(() => { + const list = props.integratedList.filter((item: any) => item.integrated).map((item: any) => item.name); + setIntegratedList(list); + }, [props.integratedList]); + + useEffect(() => { + if (!props.siteId) { + props.setSiteId(initialSiteId); + props.fetchIntegrationList(initialSiteId); + } else { + props.fetchIntegrationList(props.siteId); + } + }, []); + + const onClick = (integration: any) => { + if (integration.slug) { + props.fetch(integration.slug, props.siteId); + } + showModal(integration.component, { right: true }); + }; + + const onChangeSelect = ({ value }: any) => { + props.setSiteId(value.value); + props.fetchIntegrationList(value.value); + }; + + return ( +
+ {!hideHeader && Integrations
} />} + {integrations.map((cat: any) => ( +
+
+

{cat.title}

+ {cat.isProject && ( +
+
+ +
+ {loading && cat.isProject && } +
+ )} +
+
{cat.description}
+ +
+ {/* */} + {cat.integrations.map((integration: any) => ( + onClick(integration)} + hide={ + (integration.slug === 'github' && integratedList.includes('jira')) || + (integration.slug === 'jira' && integratedList.includes('github')) + } + /> + ))} + {/* */} +
+
+ ))} +
+ ); +} + +export default connect( + (state: any) => ({ + initialSiteId: state.getIn(['site', 'siteId']), + integratedList: state.getIn(['integrations', 'list']) || [], + loading: state.getIn(['integrations', 'fetchRequest', 'loading']), + siteId: state.getIn(['integrations', 'siteId']), + }), + { fetch, init, fetchIntegrationList, setSiteId } +)(Integrations); + +const integrations = [ + { + title: 'Issue Reporting and Collaborations', + description: 'Seamlessly report issues or share issues with your team right from OpenReplay.', + isProject: false, + integrations: [ + { title: 'Jira', slug: 'jira', category: 'Errors', icon: 'integrations/jira', component: }, + { title: 'Github', slug: 'github', category: 'Errors', icon: 'integrations/github', component: }, + { title: 'Slack', category: 'Errors', icon: 'integrations/slack', component: }, + ], + }, + { + title: 'Backend Logging', + isProject: true, + description: 'Sync your backend errors with sessions replays and see what happened front-to-back.', + integrations: [ + { title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: }, + { title: 'Datadog', slug: 'datadog', icon: 'integrations/datadog', component: }, + { title: 'Rollbar', slug: 'rollbar', icon: 'integrations/rollbar', component: }, + { title: 'Elasticsearch', slug: 'elasticsearch', icon: 'integrations/elasticsearch', component: }, + { title: 'Datadog', slug: 'datadog', icon: 'integrations/datadog', component: }, + { title: 'Sumo Logic', slug: 'sumologic', icon: 'integrations/sumologic', component: }, + { + title: 'Stackdriver', + slug: 'stackdriver', + icon: 'integrations/google-cloud', + component: , + }, + { title: 'CloudWatch', slug: 'cloudwatch', icon: 'integrations/aws', component: }, + { title: 'Newrelic', slug: 'newrelic', icon: 'integrations/newrelic', component: }, + ], + }, + { + title: 'Plugins', + isProject: false, + description: + "Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.", + integrations: [ + { title: 'Redux', slug: '', icon: 'integrations/redux', component: }, + { title: 'VueX', slug: '', icon: 'integrations/vuejs', component: }, + { title: 'GraphQL', slug: '', icon: 'integrations/graphql', component: }, + { title: 'NgRx', slug: '', icon: 'integrations/ngrx', component: }, + { title: 'MobX', slug: '', icon: 'integrations/mobx', component: }, + { title: 'Fetch', slug: '', icon: 'integrations/openreplay', component: }, + { title: 'Profiler', slug: '', icon: 'integrations/openreplay', component: }, + { title: 'Axios', slug: '', icon: 'integrations/openreplay', component: }, + { title: 'Assist', slug: '', icon: 'integrations/openreplay', component: }, + ], + }, +]; diff --git a/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js b/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js index dc4585872..b17bbc460 100644 --- a/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js +++ b/frontend/app/components/Client/Integrations/JiraForm/JiraForm.js @@ -1,37 +1,41 @@ import React from 'react'; -import IntegrationForm from '../IntegrationForm'; +import IntegrationForm from '../IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const JiraForm = (props) => ( - <> -
-
How to integrate Jira Cloud with OpenReplay.
-
- -
+
+

Jira

+
+
How to integrate Jira Cloud with OpenReplay.
+
+ +
+
+
- - ); -JiraForm.displayName = "JiraForm"; +JiraForm.displayName = 'JiraForm'; -export default JiraForm; \ No newline at end of file +export default JiraForm; diff --git a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js index 320e1a742..bbe36d45b 100644 --- a/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js +++ b/frontend/app/components/Client/Integrations/MobxDoc/MobxDoc.js @@ -1,29 +1,35 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const MobxDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-mobx --save`} - - -
Usage
-

Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux chain.

-
+ const { projectKey } = props; + return ( +
+

MobX

+
+
+ This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful + for understanding and fixing issues. +
-
Usage
- - {`import OpenReplay from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-mobx --save`} + +
Usage
+

+ Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux + chain. +

+
+ +
Usage
+ + {`import OpenReplay from '@openreplay/tracker'; import trackerMobX from '@openreplay/tracker-mobx'; //... const tracker = new OpenReplay({ @@ -31,11 +37,11 @@ const tracker = new OpenReplay({ }); tracker.use(trackerMobX()); // check list of available options below tracker.start();`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerMobX from '@openreplay/tracker-mobx/cjs'; //... const tracker = new OpenReplay({ @@ -48,15 +54,16 @@ function SomeFunctionalComponent() { tracker.start(); }, []) }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -MobxDoc.displayName = "MobxDoc"; +MobxDoc.displayName = 'MobxDoc'; export default MobxDoc; diff --git a/frontend/app/components/Client/Integrations/NewrelicForm/NewrelicForm.js b/frontend/app/components/Client/Integrations/NewrelicForm/NewrelicForm.js index d7ce557e8..670656583 100644 --- a/frontend/app/components/Client/Integrations/NewrelicForm/NewrelicForm.js +++ b/frontend/app/components/Client/Integrations/NewrelicForm/NewrelicForm.js @@ -1,32 +1,36 @@ import React from 'react'; -import IntegrationForm from '../IntegrationForm'; +import IntegrationForm from '../IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const NewrelicForm = (props) => ( - <> -
-
How to integrate NewRelic with OpenReplay and see backend errors alongside session recordings.
- +
+

New Relic

+
+
How to integrate NewRelic with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -NewrelicForm.displayName = "NewrelicForm"; +NewrelicForm.displayName = 'NewrelicForm'; -export default NewrelicForm; \ No newline at end of file +export default NewrelicForm; diff --git a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js index 385b0d4e4..956e4f57e 100644 --- a/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js +++ b/frontend/app/components/Client/Integrations/NgRxDoc/NgRxDoc.js @@ -1,29 +1,32 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const NgRxDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-ngrx --save`} - - -
Usage
-

Add the generated meta-reducer into your imports. See NgRx documentation for more details.

-
+ const { projectKey } = props; + return ( +
+

NgRx

+
+
+ This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very + useful for understanding and fixing issues. +
-
Usage
- - {`import { StoreModule } from '@ngrx/store'; +
Installation
+ {`npm i @openreplay/tracker-ngrx --save`} + +
Usage
+

Add the generated meta-reducer into your imports. See NgRx documentation for more details.

+
+ +
Usage
+ + {`import { StoreModule } from '@ngrx/store'; import { reducers } from './reducers'; import OpenReplay from '@openreplay/tracker'; import trackerNgRx from '@openreplay/tracker-ngrx'; @@ -39,11 +42,11 @@ const metaReducers = [tracker.use(trackerNgRx())]; // check list of ava imports: [StoreModule.forRoot(reducers, { metaReducers })] }) export class AppModule {}`} - - } - second={ - - {`import { StoreModule } from '@ngrx/store'; + + } + second={ + + {`import { StoreModule } from '@ngrx/store'; import { reducers } from './reducers'; import OpenReplay from '@openreplay/tracker/cjs'; import trackerNgRx from '@openreplay/tracker-ngrx/cjs'; @@ -64,15 +67,16 @@ const metaReducers = [tracker.use(trackerNgRx())]; // check list of ava }) export class AppModule {} }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -NgRxDoc.displayName = "NgRxDoc"; +NgRxDoc.displayName = 'NgRxDoc'; export default NgRxDoc; diff --git a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js index 9cada092b..f5ffab724 100644 --- a/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js +++ b/frontend/app/components/Client/Integrations/ProfilerDoc/ProfilerDoc.js @@ -1,29 +1,32 @@ import React from 'react'; -import Highlight from 'react-highlight' -import ToggleContent from 'Shared/ToggleContent' +import Highlight from 'react-highlight'; +import ToggleContent from 'Shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const ProfilerDoc = (props) => { - const { projectKey } = props; - return ( -
-
The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function call.
- -
Installation
- - {`npm i @openreplay/tracker-profiler --save`} - - -
Usage
-

Initialize the tracker and load the plugin into it. Then decorate any function inside your code with the generated function.

-
+ const { projectKey } = props; + return ( +
+

Profiler

+
+
+ The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function + call. +
-
Usage
- - {`import OpenReplay from '@openreplay/tracker'; +
Installation
+ {`npm i @openreplay/tracker-profiler --save`} + +
Usage
+

Initialize the tracker and load the plugin into it. Then decorate any function inside your code with the generated function.

+
+ +
Usage
+ + {`import OpenReplay from '@openreplay/tracker'; import trackerProfiler from '@openreplay/tracker-profiler'; //... const tracker = new OpenReplay({ @@ -36,11 +39,11 @@ export const profiler = tracker.use(trackerProfiler()); const fn = profiler('call_name')(() => { //... }, thisArg); // thisArg is optional`} - - } - second={ - - {`import OpenReplay from '@openreplay/tracker/cjs'; + + } + second={ + + {`import OpenReplay from '@openreplay/tracker/cjs'; import trackerProfiler from '@openreplay/tracker-profiler/cjs'; //... const tracker = new OpenReplay({ @@ -58,15 +61,16 @@ const fn = profiler('call_name')(() => { //... }, thisArg); // thisArg is optional }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -ProfilerDoc.displayName = "ProfilerDoc"; +ProfilerDoc.displayName = 'ProfilerDoc'; export default ProfilerDoc; diff --git a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js index 8e3b12432..e16eecbba 100644 --- a/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js +++ b/frontend/app/components/Client/Integrations/ReduxDoc/ReduxDoc.js @@ -1,28 +1,31 @@ import React from 'react'; -import Highlight from 'react-highlight' +import Highlight from 'react-highlight'; import ToggleContent from '../../../shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const ReduxDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-redux --save`} - - + const { projectKey } = props; + return ( +
+

Redux

-
Usage
-

Initialize the tracker then put the generated middleware into your Redux chain.

-
- - {`import { applyMiddleware, createStore } from 'redux'; +
+
+ This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very + useful for understanding and fixing issues. +
+ +
Installation
+ {`npm i @openreplay/tracker-redux --save`} + +
Usage
+

Initialize the tracker then put the generated middleware into your Redux chain.

+
+ + {`import { applyMiddleware, createStore } from 'redux'; import OpenReplay from '@openreplay/tracker'; import trackerRedux from '@openreplay/tracker-redux'; //... @@ -35,11 +38,11 @@ const store = createStore( reducer, applyMiddleware(tracker.use(trackerRedux())) // check list of available options below );`} - - } - second={ - - {`import { applyMiddleware, createStore } from 'redux'; + + } + second={ + + {`import { applyMiddleware, createStore } from 'redux'; import OpenReplay from '@openreplay/tracker/cjs'; import trackerRedux from '@openreplay/tracker-redux/cjs'; //... @@ -57,15 +60,16 @@ const store = createStore( applyMiddleware(tracker.use(trackerRedux())) // check list of available options below ); }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -ReduxDoc.displayName = "ReduxDoc"; +ReduxDoc.displayName = 'ReduxDoc'; export default ReduxDoc; diff --git a/frontend/app/components/Client/Integrations/RollbarForm.js b/frontend/app/components/Client/Integrations/RollbarForm.js index 3b8830423..441819323 100644 --- a/frontend/app/components/Client/Integrations/RollbarForm.js +++ b/frontend/app/components/Client/Integrations/RollbarForm.js @@ -1,25 +1,27 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const RollbarForm = (props) => ( - <> -
-
How to integrate Rollbar with OpenReplay and see backend errors alongside session replays.
- +
+

Rollbar

+
+
How to integrate Rollbar with OpenReplay and see backend errors alongside session replays.
+ +
+
- - ); -RollbarForm.displayName = "RollbarForm"; +RollbarForm.displayName = 'RollbarForm'; -export default RollbarForm; \ No newline at end of file +export default RollbarForm; diff --git a/frontend/app/components/Client/Integrations/SentryForm.js b/frontend/app/components/Client/Integrations/SentryForm.js index fd7bf1f11..bd119ba31 100644 --- a/frontend/app/components/Client/Integrations/SentryForm.js +++ b/frontend/app/components/Client/Integrations/SentryForm.js @@ -1,31 +1,35 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const SentryForm = (props) => ( - <> -
-
How to integrate Sentry with OpenReplay and see backend errors alongside session recordings.
- +
+

Sentry

+
+
How to integrate Sentry with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -SentryForm.displayName = "SentryForm"; +SentryForm.displayName = 'SentryForm'; -export default SentryForm; \ No newline at end of file +export default SentryForm; diff --git a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js index 8e1bb121e..f018da3e5 100644 --- a/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js +++ b/frontend/app/components/Client/Integrations/SlackAddForm/SlackAddForm.js @@ -1,101 +1,91 @@ -import React from 'react' -import { connect } from 'react-redux' -import { edit, save, init, update } from 'Duck/integrations/slack' -import { Form, Input, Button, Message } from 'UI' +import React from 'react'; +import { connect } from 'react-redux'; +import { edit, save, init, update } from 'Duck/integrations/slack'; +import { Form, Input, Button, Message } from 'UI'; import { confirm } from 'UI'; -import { remove } from 'Duck/integrations/slack' +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) - } - } - - remove = async (id) => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this channel?` - })) { - this.props.remove(id); + componentWillUnmount() { + this.props.init({}); } - } - write = ({ target: { name, value } }) => this.props.edit({ [ name ]: value }); - - render() { - const { instance, saving, errors, onClose } = this.props; - return ( -
-
- - - - - - - - -
-
- - - -
- - -
-
- - { errors && -
- { errors.map(error => { error }) } -
+ save = () => { + const instance = this.props.instance; + if (instance.exists()) { + this.props.update(this.props.instance); + } else { + this.props.save(this.props.instance); } -
- ) - } + }; + + remove = async (id) => { + if ( + await confirm({ + header: 'Confirm', + confirmButton: 'Yes, delete', + confirmation: `Are you sure you want to permanently delete this channel?`, + }) + ) { + this.props.remove(id); + } + }; + + write = ({ target: { name, value } }) => this.props.edit({ [name]: value }); + + render() { + const { instance, saving, errors, onClose } = this.props; + return ( +
+
+ + + + + + + + +
+
+ + + +
+ + +
+
+ + {errors && ( +
+ {errors.map((error) => ( + + {error} + + ))} +
+ )} +
+ ); + } } -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 +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); diff --git a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js index f78527204..8d25b4454 100644 --- a/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js +++ b/frontend/app/components/Client/Integrations/SlackChannelList/SlackChannelList.js @@ -1,49 +1,51 @@ -import React from 'react' -import { connect } from 'react-redux' +import React from 'react'; +import { connect } from 'react-redux'; import { NoContent } from 'UI'; -import { remove, edit } from 'Duck/integrations/slack' +import { remove, edit, init } from 'Duck/integrations/slack'; import DocLink from 'Shared/DocLink/DocLink'; function SlackChannelList(props) { - const { list } = props; + const { list } = props; - const onEdit = (instance) => { - props.edit(instance) - props.onEdit() - } + const onEdit = (instance) => { + props.edit(instance); + props.onEdit(); + }; - 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 } - > - {list.map(c => ( -
onEdit(c)} - > -
-
{c.name}
-
- {c.endpoint} -
-
-
- ))} - -
- ) + 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} + > + {list.map((c) => ( +
onEdit(c)} + > +
+
{c.name}
+
{c.endpoint}
+
+
+ ))} + +
+ ); } -export default connect(state => ({ - list: state.getIn(['slack', 'list']) -}), { remove, edit })(SlackChannelList) +export default connect( + (state) => ({ + list: state.getIn(['slack', 'list']), + }), + { remove, edit, init } +)(SlackChannelList); diff --git a/frontend/app/components/Client/Integrations/SlackForm.js b/frontend/app/components/Client/Integrations/SlackForm.js deleted file mode 100644 index 986af20ab..000000000 --- a/frontend/app/components/Client/Integrations/SlackForm.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import SlackChannelList from './SlackChannelList/SlackChannelList'; - -const SlackForm = (props) => { - const { onEdit } = props; - return ( - <> - - - ) -} - -SlackForm.displayName = "SlackForm"; - -export default SlackForm; \ No newline at end of file diff --git a/frontend/app/components/Client/Integrations/SlackForm.tsx b/frontend/app/components/Client/Integrations/SlackForm.tsx new file mode 100644 index 000000000..7d0cdc610 --- /dev/null +++ b/frontend/app/components/Client/Integrations/SlackForm.tsx @@ -0,0 +1,58 @@ +import React, { useEffect } from 'react'; +import SlackChannelList from './SlackChannelList/SlackChannelList'; +import { fetchList, init } from 'Duck/integrations/slack'; +import { connect } from 'react-redux'; +import SlackAddForm from './SlackAddForm'; +import { useModal } from 'App/components/Modal'; +import { Button } from 'UI'; + +interface Props { + onEdit: (integration: any) => void; + istance: any; + fetchList: any; + init: any; +} +const SlackForm = (props: Props) => { + const { istance } = props; + const { hideModal } = useModal(); + const [active, setActive] = React.useState(false); + + const onEdit = () => { + setActive(true); + }; + + const onNew = () => { + setActive(true); + props.init({}); + } + + useEffect(() => { + props.fetchList(); + }, []); + + return ( +
+ {active && ( +
+ setActive(false)} /> +
+ )} +
+
+

Slack

+
+ +
+
+ ); +}; + +SlackForm.displayName = 'SlackForm'; + +export default connect( + (state: any) => ({ + istance: state.getIn(['slack', 'instance']), + }), + { fetchList, init } +)(SlackForm); diff --git a/frontend/app/components/Client/Integrations/StackdriverForm.js b/frontend/app/components/Client/Integrations/StackdriverForm.js index b8e29fa3c..ce137bd99 100644 --- a/frontend/app/components/Client/Integrations/StackdriverForm.js +++ b/frontend/app/components/Client/Integrations/StackdriverForm.js @@ -1,29 +1,32 @@ import React from 'react'; -import IntegrationForm from './IntegrationForm'; +import IntegrationForm from './IntegrationForm'; import DocLink from 'Shared/DocLink/DocLink'; const StackdriverForm = (props) => ( - <> -
-
How to integrate Stackdriver with OpenReplay and see backend errors alongside session recordings.
- +
+

Stackdriver

+
+
How to integrate Stackdriver with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -StackdriverForm.displayName = "StackdriverForm"; +StackdriverForm.displayName = 'StackdriverForm'; export default StackdriverForm; diff --git a/frontend/app/components/Client/Integrations/SumoLogicForm/SumoLogicForm.js b/frontend/app/components/Client/Integrations/SumoLogicForm/SumoLogicForm.js index 0a807edb6..6aea9fe6e 100644 --- a/frontend/app/components/Client/Integrations/SumoLogicForm/SumoLogicForm.js +++ b/frontend/app/components/Client/Integrations/SumoLogicForm/SumoLogicForm.js @@ -4,30 +4,34 @@ import RegionDropdown from './RegionDropdown'; import DocLink from 'Shared/DocLink/DocLink'; const SumoLogicForm = (props) => ( - <> -
-
How to integrate SumoLogic with OpenReplay and see backend errors alongside session recordings.
- +
+

Sumologic

+
+
How to integrate SumoLogic with OpenReplay and see backend errors alongside session recordings.
+ +
+
- - ); -SumoLogicForm.displayName = "SumoLogicForm"; +SumoLogicForm.displayName = 'SumoLogicForm'; export default SumoLogicForm; diff --git a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js index e00d1c0ad..cece7c01e 100644 --- a/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js +++ b/frontend/app/components/Client/Integrations/VueDoc/VueDoc.js @@ -1,29 +1,34 @@ import React from 'react'; -import Highlight from 'react-highlight' +import Highlight from 'react-highlight'; import ToggleContent from '../../../shared/ToggleContent'; import DocLink from 'Shared/DocLink/DocLink'; const VueDoc = (props) => { - const { projectKey } = props; - return ( -
-
This plugin allows you to capture VueX mutations/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.
- -
Installation
- - {`npm i @openreplay/tracker-vuex --save`} - - -
Usage
-

Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated plugin into your plugins field of your store.

-
+ const { projectKey } = props; + return ( +
+

VueX

+
+
+ This plugin allows you to capture VueX mutations/state and inspect them later on while replaying session recordings. This is very + useful for understanding and fixing issues. +
- - - {`import Vuex from 'vuex' +
Installation
+ {`npm i @openreplay/tracker-vuex --save`} + +
Usage
+

+ Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated plugin into your plugins + field of your store. +

+
+ + + {`import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker'; import trackerVuex from '@openreplay/tracker-vuex'; //... @@ -36,11 +41,11 @@ const store = new Vuex.Store({ //... plugins: [tracker.use(trackerVuex())] // check list of available options below });`} - - } - second={ - - {`import Vuex from 'vuex' + + } + second={ + + {`import Vuex from 'vuex' import OpenReplay from '@openreplay/tracker/cjs'; import trackerVuex from '@openreplay/tracker-vuex/cjs'; //... @@ -58,15 +63,16 @@ const store = new Vuex.Store({ plugins: [tracker.use(trackerVuex())] // check list of available options below }); }`} - - } - /> + + } + /> - -
- ) + +
+
+ ); }; -VueDoc.displayName = "VueDoc"; +VueDoc.displayName = 'VueDoc'; export default VueDoc; diff --git a/frontend/app/components/Client/Integrations/_IntegrationItem .js_old b/frontend/app/components/Client/Integrations/_IntegrationItem .js_old deleted file mode 100644 index 962135633..000000000 --- a/frontend/app/components/Client/Integrations/_IntegrationItem .js_old +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import { Icon } from 'UI'; -import styles from './integrationItem.module.css'; - -const onDocLinkClick = (e, link) => { - e.stopPropagation(); - window.open(link, '_blank'); -} - -const IntegrationItem = ({ - deleteHandler = null, icon, url = null, title = '', description = '', onClick = null, dockLink = '', integrated = false -}) => { - return ( -
onClick(e, url) }> - -

{ title }

-

{ description }

-
-
- {deleteHandler && ( -
- - { 'Remove' } -
- )} - { dockLink && ( -
onDocLinkClick(e, dockLink) }> - - { 'Documentation' } -
- )} -
- - { 'Integrated' } -
-
-
- ) -}; - -export default IntegrationItem; diff --git a/frontend/app/components/Client/Integrations/integrationItem.module.css b/frontend/app/components/Client/Integrations/integrationItem.module.css index 94ab26726..fca162909 100644 --- a/frontend/app/components/Client/Integrations/integrationItem.module.css +++ b/frontend/app/components/Client/Integrations/integrationItem.module.css @@ -9,7 +9,7 @@ display: flex; flex-direction: column; align-items: center; - justify-content: center; + justify-content: flex-start; /* min-height: 250px; */ /* min-width: 260px; */ /* max-width: 300px; */ diff --git a/frontend/app/components/Client/Notifications/Notifications.js b/frontend/app/components/Client/Notifications/Notifications.js index 15d6b9b4d..d01b12456 100644 --- a/frontend/app/components/Client/Notifications/Notifications.js +++ b/frontend/app/components/Client/Notifications/Notifications.js @@ -1,46 +1,50 @@ -import React, { useEffect } from 'react' -import cn from 'classnames' -import stl from './notifications.module.css' -import { Checkbox } from 'UI' -import { connect } from 'react-redux' -import { withRequest } from 'HOCs' -import { fetch as fetchConfig, edit as editConfig, save as saveConfig } from 'Duck/config' +import React, { useEffect } from 'react'; +import cn from 'classnames'; +import stl from './notifications.module.css'; +import { Checkbox, Toggler } from 'UI'; +import { connect } from 'react-redux'; +import { withRequest } from 'HOCs'; +import { fetch as fetchConfig, edit as editConfig, save as saveConfig } from 'Duck/config'; import withPageTitle from 'HOCs/withPageTitle'; function Notifications(props) { - const { config } = props; + const { config } = props; - useEffect(() => { - props.fetchConfig(); - }, []) + useEffect(() => { + props.fetchConfig(); + }, []); - const onChange = () => { - const _config = { 'weeklyReport' : !config.weeklyReport }; - props.editConfig(_config); - props.saveConfig(_config) - } + const onChange = () => { + const _config = { weeklyReport: !config.weeklyReport }; + props.editConfig(_config); + props.saveConfig(_config); + }; - return ( -
-
- {

{ 'Notifications' }

} -
-
- - -
-
- ) + return ( +
+
{

{'Notifications'}

}
+
+
Weekly project summary
+
Receive wekly report for each project on email.
+ + {/* */} + {/* */} +
+
+ ); } -export default connect(state => ({ - config: state.getIn(['config', 'options']) -}), { fetchConfig, editConfig, saveConfig })(withPageTitle('Notifications - OpenReplay Preferences')(Notifications)); +export default connect( + (state) => ({ + config: state.getIn(['config', 'options']), + }), + { fetchConfig, editConfig, saveConfig } +)(withPageTitle('Notifications - OpenReplay Preferences')(Notifications)); diff --git a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js index 820fe14e4..8314e521a 100644 --- a/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js +++ b/frontend/app/components/Client/PreferencesMenu/PreferencesMenu.js @@ -13,14 +13,14 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) { }; return ( -
+
Preferences
-
+
-
+
-
+
{ -
+
} -
+
{isEnterprise && isAdmin && ( -
+
+
- setTab(CLIENT_TABS.MANAGE_USERS)} - /> -
+
+ setTab(CLIENT_TABS.MANAGE_USERS)} + /> +
)} -
+
newPasswordRepeat.length > 0 && newPasswordRepeat !== newPassword; const defaultState = { - oldPassword: '', - newPassword: '', - newPasswordRepeat: '', - success: false, + oldPassword: '', + newPassword: '', + newPasswordRepeat: '', + success: false, + show: false, }; -@connect(state => ({ - passwordErrors: state.getIn(['user', 'passwordErrors']), - loading: state.getIn(['user', 'updatePasswordRequest', 'loading']) -}), { - updatePassword -}) +@connect( + (state) => ({ + passwordErrors: state.getIn(['user', 'passwordErrors']), + loading: state.getIn(['user', 'updatePasswordRequest', 'loading']), + }), + { + updatePassword, + } +) export default class ChangePassword extends React.PureComponent { - state = defaultState + state = defaultState; - write = ({ target: { name, value } }) => { - this.setState({ - [ name ]: value, - }); - } - - handleSubmit = (e) => { - e.preventDefault(); - if (this.isSubmitDisabled()) return; - - const { oldPassword, newPassword } = this.state; - this.setState({ - success: false, - }); - - this.props.updatePassword({ - oldPassword, - newPassword, - }).then(() => { - if (this.props.passwordErrors.size === 0) { + write = ({ target: { name, value } }) => { this.setState({ - ...defaultState, - success: true, + [name]: value, }); - } - }); - } + }; - isSubmitDisabled() { - const { oldPassword, newPassword, newPasswordRepeat } = this.state; - if (newPassword !== newPasswordRepeat || - newPassword.length < MIN_LENGTH || - oldPassword.length < MIN_LENGTH) return true; - return false; - } + handleSubmit = (e) => { + e.preventDefault(); + if (this.isSubmitDisabled()) return; - render() { - const { - oldPassword, newPassword, newPasswordRepeat, success - } = this.state; - const { loading, passwordErrors } = this.props; + const { oldPassword, newPassword } = this.state; + this.setState({ + success: false, + }); - const doesntMatch = checkDoesntMatch(newPassword, newPasswordRepeat); - return ( -
- - - - - - - -
- { PASSWORD_POLICY } -
-
- - - - - { passwordErrors.map(err => ( - - { err } - - ))} - - - -
- ); - } + this.props + .updatePassword({ + oldPassword, + newPassword, + }) + .then(() => { + if (this.props.passwordErrors.size === 0) { + this.setState({ + ...defaultState, + success: true, + }); + } + }); + }; + + isSubmitDisabled() { + const { oldPassword, newPassword, newPasswordRepeat } = this.state; + if (newPassword !== newPasswordRepeat || newPassword.length < MIN_LENGTH || oldPassword.length < MIN_LENGTH) return true; + return false; + } + + render() { + const { oldPassword, newPassword, newPasswordRepeat, success, show } = this.state; + const { loading, passwordErrors } = this.props; + + const doesntMatch = checkDoesntMatch(newPassword, newPasswordRepeat); + return show ? ( +
+ + + + + + + +
{PASSWORD_POLICY}
+
+ + + + + {passwordErrors.map((err) => ( + {err} + ))} + +
+ + + +
+ +
+ ) : ( +
this.setState({ show: true })}> + +
+ ); + } } diff --git a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js b/frontend/app/components/Client/ProfileSettings/ProfileSettings.js index 375e3ba8e..7e4ec5fb2 100644 --- a/frontend/app/components/Client/ProfileSettings/ProfileSettings.js +++ b/frontend/app/components/Client/ProfileSettings/ProfileSettings.js @@ -8,90 +8,105 @@ import TenantKey from './TenantKey'; import OptOut from './OptOut'; import Licenses from './Licenses'; import { connect } from 'react-redux'; +import { PageTitle } from 'UI'; @withPageTitle('Account - OpenReplay Preferences') -@connect(state => ({ - account: state.getIn([ 'user', 'account' ]), - isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee', +@connect((state) => ({ + account: state.getIn(['user', 'account']), + isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', })) -export default class ProfileSettings extends React.PureComponent { - render() { - const { account, isEnterprise } = this.props; - return ( - -
-
-

{ 'Profile' }

-
{ 'Your email address is your identity on OpenReplay and is used to login.' }
-
-
-
+export default class ProfileSettings extends React.PureComponent { + render() { + const { account, isEnterprise } = this.props; + return ( + + Account
} /> +
+
+

{'Profile'}

+
{'Your email address is your identity on OpenReplay and is used to login.'}
+
+
+ +
+
-
+
- { account.hasPassword && ( - <> -
-
-

{ 'Change Password' }

-
{ 'Updating your password from time to time enhances your account’s security.' }
-
-
-
- + {account.hasPassword && ( + <> +
+
+

{'Change Password'}

+
{'Updating your password from time to time enhances your account’s security.'}
+
+
+ +
+
-
- - )} +
+ + )} -
-
-

{ 'Organization API Key' }

-
{ 'Your API key gives you access to an extra set of services.' }
-
-
-
+
+
+

{'Organization API Key'}

+
{'Your API key gives you access to an extra set of services.'}
+
+
+ +
+
- { isEnterprise && ( - <> -
-
-
-

{ 'Tenant Key' }

-
{ 'For SSO (SAML) authentication.' }
-
-
-
- - )} + {isEnterprise && ( + <> +
+
+
+

{'Tenant Key'}

+
{'For SSO (SAML) authentication.'}
+
+
+ +
+
+ + )} - { !isEnterprise && ( - <> -
-
-
-

{ 'Data Collection' }

-
{ 'Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.' }
-
-
-
- - )} + {!isEnterprise && ( + <> +
+
+
+

{'Data Collection'}

+
+ {'Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.'} +
+
+
+ +
+
+ + )} - { account.license && ( - <> -
+ {account.license && ( + <> +
-
-
-

{ 'License' }

-
{ 'License key and expiration date.' }
-
-
-
- - )} - - ); - } +
+
+

{'License'}

+
{'License key and expiration date.'}
+
+
+ +
+
+ + )} + + ); + } } diff --git a/frontend/app/components/Client/Roles/Roles.tsx b/frontend/app/components/Client/Roles/Roles.tsx index f9b9ef072..6ed98ac06 100644 --- a/frontend/app/components/Client/Roles/Roles.tsx +++ b/frontend/app/components/Client/Roles/Roles.tsx @@ -1,156 +1,154 @@ -import React, { useState, useEffect } from 'react' -import cn from 'classnames' -import { Loader, IconButton, Popup, NoContent, SlideModal } from 'UI' -import { connect } from 'react-redux' -import stl from './roles.module.css' -import RoleForm from './components/RoleForm' +import React, { useState, useEffect } from 'react'; +import cn from 'classnames'; +import { Loader, IconButton, Popup, NoContent, SlideModal } from 'UI'; +import { connect } from 'react-redux'; +import stl from './roles.module.css'; +import RoleForm from './components/RoleForm'; import { init, edit, fetchList, remove as deleteRole, resetErrors } from 'Duck/roles'; -import RoleItem from './components/RoleItem' +import RoleItem from './components/RoleItem'; import { confirm } from 'UI'; import { toast } from 'react-toastify'; import withPageTitle from 'HOCs/withPageTitle'; +import { useModal } from 'App/components/Modal'; interface Props { - loading: boolean - init: (role?: any) => void, - edit: (role: any) => void, - instance: any, - roles: any[], - deleteRole: (id: any) => Promise, - fetchList: () => Promise, - account: any, - permissionsMap: any, - removeErrors: any, - resetErrors: () => void, - projectsMap: any, + loading: boolean; + init: (role?: any) => void; + edit: (role: any) => void; + instance: any; + roles: any[]; + deleteRole: (id: any) => Promise; + fetchList: () => Promise; + account: any; + permissionsMap: any; + removeErrors: any; + resetErrors: () => void; + projectsMap: any; } function Roles(props: Props) { - const { loading, instance, roles, init, edit, deleteRole, account, permissionsMap, projectsMap, removeErrors } = props - const [showModal, setShowmModal] = useState(false) - const isAdmin = account.admin || account.superAdmin; + const { loading, instance, roles, init, edit, deleteRole, account, permissionsMap, projectsMap, removeErrors } = props; + // const [showModal, setShowmModal] = useState(false); + const { showModal, hideModal } = useModal(); + const isAdmin = account.admin || account.superAdmin; - useEffect(() => { - props.fetchList() - }, []) + useEffect(() => { + props.fetchList(); + }, []); - useEffect(() => { - if (removeErrors && removeErrors.size > 0) { - removeErrors.forEach(e => { - toast.error(e) - }) - } - return () => { - props.resetErrors() - } - }, [removeErrors]) + useEffect(() => { + if (removeErrors && removeErrors.size > 0) { + removeErrors.forEach((e) => { + toast.error(e); + }); + } + return () => { + props.resetErrors(); + }; + }, [removeErrors]); - const closeModal = (showToastMessage) => { - if (showToastMessage) { - toast.success(showToastMessage) - props.fetchList() - } - setShowmModal(false) - setTimeout(() => { - init() - }, 100) - } + const closeModal = (showToastMessage) => { + if (showToastMessage) { + toast.success(showToastMessage); + props.fetchList(); + } + setShowmModal(false); + setTimeout(() => { + init(); + }, 100); + }; - const editHandler = role => { - init(role) - setShowmModal(true) - } + const editHandler = (role: any) => { + init(role); + showModal(, { right: true }); + // setShowmModal(true); + }; - const deleteHandler = async (role) => { - if (await confirm({ - header: 'Roles', - confirmation: `Are you sure you want to remove this role?` - })) { - deleteRole(role.roleId) - } - } + const deleteHandler = async (role: any) => { + if ( + await confirm({ + header: 'Roles', + confirmation: `Are you sure you want to remove this role?`, + }) + ) { + deleteRole(role.roleId); + } + }; - return ( - - - } - onClose={ closeModal } - /> -
-
-
-

Roles and Access

- -
- setShowmModal(true) } - /> + return ( + + + {/* } + onClose={closeModal} + /> */} +
+
+
+

Roles and Access

+ +
+ setShowmModal(true)} /> +
+
+
+
+ + +
+
+
+ Title +
+
+ Project Access +
+
+ Feature Access +
+
+
+ {roles.map((role) => ( + + ))} +
+
- -
-
- - -
-
-
Title
-
Project Access
-
Feature Access
-
-
- {roles.map(role => ( - - ))} -
-
-
- - - ) + + + ); } -export default connect(state => { - const permissions = state.getIn(['roles', 'permissions']) - const permissionsMap = {} - permissions.forEach(p => { - permissionsMap[p.value] = p.text - }); - const projects = state.getIn([ 'site', 'list' ]) - return { - instance: state.getIn(['roles', 'instance']) || null, - permissionsMap: permissionsMap, - roles: state.getIn(['roles', 'list']), - removeErrors: state.getIn(['roles', 'removeRequest', 'errors']), - loading: state.getIn(['roles', 'fetchRequest', 'loading']), - account: state.getIn([ 'user', 'account' ]), - projectsMap: projects.reduce((acc, p) => { - acc[ p.get('id') ] = p.get('name') - return acc - } - , {}), - } -}, { init, edit, fetchList, deleteRole, resetErrors })(withPageTitle('Roles & Access - OpenReplay Preferences')(Roles)) \ No newline at end of file +export default connect( + (state: any) => { + const permissions = state.getIn(['roles', 'permissions']); + const permissionsMap = {}; + permissions.forEach((p: any) => { + permissionsMap[p.value] = p.text; + }); + const projects = state.getIn(['site', 'list']); + return { + instance: state.getIn(['roles', 'instance']) || null, + permissionsMap: permissionsMap, + roles: state.getIn(['roles', 'list']), + removeErrors: state.getIn(['roles', 'removeRequest', 'errors']), + loading: state.getIn(['roles', 'fetchRequest', 'loading']), + account: state.getIn(['user', 'account']), + projectsMap: projects.reduce((acc: any, p: any) => { + acc[p.get('id')] = p.get('name'); + return acc; + }, {}), + }; + }, + { init, edit, fetchList, deleteRole, resetErrors } +)(withPageTitle('Roles & Access - OpenReplay Preferences')(Roles)); diff --git a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx index 7aed70131..93a320d54 100644 --- a/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx +++ b/frontend/app/components/Client/Roles/components/RoleForm/RoleForm.tsx @@ -1,203 +1,195 @@ -import React, { useRef, useEffect } from 'react' -import { connect } from 'react-redux' -import stl from './roleForm.module.css' -import { save, edit } from 'Duck/roles' -import { Form, Input, Button, Checkbox, Icon } from 'UI' +import React, { useRef, useEffect } from 'react'; +import { connect } from 'react-redux'; +import stl from './roleForm.module.css'; +import { save, edit } from 'Duck/roles'; +import { Form, Input, Button, Checkbox, Icon } from 'UI'; import Select from 'Shared/Select'; interface Permission { - name: string, - value: string + name: string; + value: string; } interface Props { - role: any, - edit: (role: any) => void, - save: (role: any) => Promise, - closeModal: (toastMessage?: string) => void, - saving: boolean, - permissions: Array[] - projectOptions: Array[], - permissionsMap: any, - projectsMap: any, - deleteHandler: (id: any) => Promise, + role: any; + edit: (role: any) => void; + save: (role: any) => Promise; + closeModal: (toastMessage?: string) => void; + saving: boolean; + permissions: Array[]; + projectOptions: Array[]; + permissionsMap: any; + projectsMap: any; + deleteHandler: (id: any) => Promise; } const RoleForm = (props: Props) => { - const { role, edit, save, closeModal, saving, permissions, projectOptions, permissionsMap, projectsMap } = props - let focusElement = useRef(null) - const _save = () => { - save(role).then(() => { - closeModal(role.exists() ? "Role updated" : "Role created"); - }) - } + const { role, edit, save, closeModal, saving, permissions, projectOptions, permissionsMap, projectsMap } = props; + let focusElement = useRef(null); + const _save = () => { + save(role).then(() => { + closeModal(role.exists() ? 'Role updated' : 'Role created'); + }); + }; - const write = ({ target: { value, name } }) => edit({ [ name ]: value }) + const write = ({ target: { value, name } }) => edit({ [name]: value }); - const onChangePermissions = (e) => { - const { permissions } = role - const index = permissions.indexOf(e) - const _perms = permissions.contains(e) ? permissions.remove(index) : permissions.push(e) - edit({ permissions: _perms }) - } + const onChangePermissions = (e) => { + const { permissions } = role; + const index = permissions.indexOf(e); + const _perms = permissions.contains(e) ? permissions.remove(index) : permissions.push(e); + edit({ permissions: _perms }); + }; - const onChangeProjects = (e) => { - const { projects } = role - const index = projects.indexOf(e) - const _projects = index === -1 ? projects.push(e) : projects.remove(index) - edit({ projects: _projects }) - } + const onChangeProjects = (e) => { + const { projects } = role; + const index = projects.indexOf(e); + const _projects = index === -1 ? projects.push(e) : projects.remove(index); + edit({ projects: _projects }); + }; - const writeOption = ({ name, value }: any) => { - if (name === 'permissions') { - onChangePermissions(value) - } else if (name === 'projects') { - onChangeProjects(value) - } - } + const writeOption = ({ name, value }: any) => { + if (name === 'permissions') { + onChangePermissions(value); + } else if (name === 'projects') { + onChangeProjects(value); + } + }; - const toggleAllProjects = () => { - const { allProjects } = role - edit({ allProjects: !allProjects }) - } + const toggleAllProjects = () => { + const { allProjects } = role; + edit({ allProjects: !allProjects }); + }; - useEffect(() => { - focusElement && focusElement.current && focusElement.current.focus() - }, []) + useEffect(() => { + focusElement && focusElement.current && focusElement.current.focus(); + }, []); - return ( -
-
- - - - + return ( +
+

{role.exists() ? 'Edit Role' : 'Create Role'}

+
+ + + + + - - + + -
- -
-
All Projects
- - (Uncheck to select specific projects) - -
-
- { !role.allProjects && ( - <> - writeOption({ name: 'projects', value: value.value })} + value={null} + /> + {role.projects.size > 0 && ( +
+ {role.projects.map((p) => OptionLabel(projectsMap, p, onChangeProjects))} +
+ )} + + )} +
+ + + + writeOption({ name: 'permissions', value: value.value }) } - value={null} - /> - { role.permissions.size > 0 && ( -
- { role.permissions.map(p => ( - OptionLabel(permissionsMap, p, onChangePermissions) - )) }
- )} -
- - -
-
- - { role.exists() && ( - - )}
- { role.exists() && ( - - )} -
-
- ); -} + ); +}; -export default connect((state: any) => { - const role = state.getIn(['roles', 'instance']) - const projects = state.getIn([ 'site', 'list' ]) - return { - role, - projectOptions: projects.map((p: any) => ({ - key: p.get('id'), - value: p.get('id'), - label: p.get('name'), - // isDisabled: role.projects.includes(p.get('id')), - })).filter(({ value }: any) => !role.projects.includes(value)).toJS(), - permissions: state.getIn(['roles', 'permissions']).filter(({ value }: any) => !role.permissions.includes(value)) - .map(({ text, value }: any) => ({ label: text, value })).toJS(), - saving: state.getIn([ 'roles', 'saveRequest', 'loading' ]), - projectsMap: projects.reduce((acc: any, p: any) => { - acc[ p.get('id') ] = p.get('name') - return acc - } - , {}), - } -}, { edit, save })(RoleForm); +export default connect( + (state: any) => { + const role = state.getIn(['roles', 'instance']); + const projects = state.getIn(['site', 'list']); + return { + role, + projectOptions: projects + .map((p: any) => ({ + key: p.get('id'), + value: p.get('id'), + label: p.get('name'), + // isDisabled: role.projects.includes(p.get('id')), + })) + .filter(({ value }: any) => !role.projects.includes(value)) + .toJS(), + permissions: state + .getIn(['roles', 'permissions']) + .filter(({ value }: any) => !role.permissions.includes(value)) + .map(({ text, value }: any) => ({ label: text, value })) + .toJS(), + saving: state.getIn(['roles', 'saveRequest', 'loading']), + projectsMap: projects.reduce((acc: any, p: any) => { + acc[p.get('id')] = p.get('name'); + return acc; + }, {}), + }; + }, + { edit, save } +)(RoleForm); function OptionLabel(nameMap: any, p: any, onChangeOption: (e: any) => void) { - return
-
{nameMap[p]}
-
onChangeOption(p)}> - -
-
+ return ( +
+
{nameMap[p]}
+
onChangeOption(p)}> + +
+
+ ); } diff --git a/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx b/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx index 845811f77..391e7ab93 100644 --- a/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx +++ b/frontend/app/components/Client/Roles/components/RoleItem/RoleItem.tsx @@ -1,64 +1,58 @@ -import React from 'react' -import { Icon, Link } from 'UI' -import stl from './roleItem.module.css' -import cn from 'classnames' +import React from 'react'; +import { Icon, Link, Button } from 'UI'; +import stl from './roleItem.module.css'; +import cn from 'classnames'; import { CLIENT_TABS, client as clientRoute } from 'App/routes'; - function PermisionLabel({ label }: any) { - return ( -
{ label }
- ); + return
{label}
; } function PermisionLabelLinked({ label, route }: any) { - return ( -
{ label }
- ); + return ( + +
{label}
+ + ); } interface Props { - role: any, - deleteHandler?: (role: any) => void, - editHandler?: (role: any) => void, - permissions: any, - isAdmin: boolean, - projects: any, + role: any; + deleteHandler?: (role: any) => void; + editHandler?: (role: any) => void; + permissions: any; + isAdmin: boolean; + projects: any; } function RoleItem({ role, deleteHandler, editHandler, isAdmin, permissions, projects }: Props) { - return ( -
-
- - { role.name } -
-
- {role.allProjects ? ( - - ) : ( - role.projects.map(p => ( - - )) - )} -
-
-
- {role.permissions.map((permission: any) => ( - - ))} -
- -
- {isAdmin && !!editHandler && -
editHandler(role) }> - + return ( +
+
+ + {role.name} +
+
+ {role.allProjects ? ( + + ) : ( + role.projects.map((p) => ) + )} +
+
+
+ {role.permissions.map((permission: any) => ( + + ))} +
+ +
+ {isAdmin && !!editHandler && ( +
- }
-
- -
- ); + ); } -export default RoleItem; \ No newline at end of file +export default RoleItem; diff --git a/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx b/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx index 7371056fd..e938b391f 100644 --- a/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx +++ b/frontend/app/components/Client/Sites/AddProjectButton/AddUserButton.tsx @@ -2,27 +2,29 @@ import React from 'react'; import { Popup, Button, IconButton } from 'UI'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; +import { init, remove, fetchGDPR } from 'Duck/site'; +import { connect } from 'react-redux'; +import { useModal } from 'App/components/Modal'; +import NewSiteForm from '../NewSiteForm'; const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.'; const LIMIT_WARNING = 'You have reached site limit.'; -function AddProjectButton({ isAdmin = false, onClick }: any) { +function AddProjectButton({ isAdmin = false, init = () => {} }: any) { const { userStore } = useStore(); + const { showModal, hideModal } = useModal(); const limtis = useObserver(() => userStore.limits); const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)); + + const onClick = () => { + init(); + showModal(, { right: true }); + }; return ( - {/* */} ); } -export default AddProjectButton; +export default connect(null, { init, remove, fetchGDPR })(AddProjectButton); diff --git a/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx b/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx new file mode 100644 index 000000000..0fe5fce65 --- /dev/null +++ b/frontend/app/components/Client/Sites/InstallButton/InstallButton.tsx @@ -0,0 +1,25 @@ +import { useModal } from 'App/components/Modal'; +import React from 'react'; +import TrackingCodeModal from 'Shared/TrackingCodeModal'; +import { Button } from 'UI'; + +interface Props { + site: any; +} +function InstallButton(props: Props) { + const { site } = props; + const { showModal, hideModal } = useModal(); + const onClick = () => { + showModal( + , + { right: true } + ); + }; + return ( + + ); +} + +export default InstallButton; diff --git a/frontend/app/components/Client/Sites/InstallButton/index.ts b/frontend/app/components/Client/Sites/InstallButton/index.ts new file mode 100644 index 000000000..c64b2ff6c --- /dev/null +++ b/frontend/app/components/Client/Sites/InstallButton/index.ts @@ -0,0 +1 @@ +export { default } from './InstallButton' \ No newline at end of file diff --git a/frontend/app/components/Client/Sites/NewSiteForm.js b/frontend/app/components/Client/Sites/NewSiteForm.js index c6633b73b..0a9dc81c7 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.js +++ b/frontend/app/components/Client/Sites/NewSiteForm.js @@ -1,121 +1,122 @@ import React from 'react'; import { connect } from 'react-redux'; import { Form, Input, Button, Icon } from 'UI'; -import { save, edit, update , fetchList, remove } from 'Duck/site'; +import { save, edit, update, fetchList, remove } from 'Duck/site'; import { pushNewSite } from 'Duck/user'; import { setSiteId } from 'Duck/site'; import { withRouter } from 'react-router-dom'; import styles from './siteForm.module.css'; import { confirm } from 'UI'; -@connect(state => ({ - site: state.getIn([ 'site', 'instance' ]), - sites: state.getIn([ 'site', 'list' ]), - siteList: state.getIn([ 'site', 'list' ]), - loading: state.getIn([ 'site', 'save', 'loading' ]) || state.getIn([ 'site', 'remove', 'loading' ]), -}), { - save, - remove, - edit, - update, - pushNewSite, - fetchList, - setSiteId -}) +@connect( + (state) => ({ + site: state.getIn(['site', 'instance']), + sites: state.getIn(['site', 'list']), + siteList: state.getIn(['site', 'list']), + loading: state.getIn(['site', 'save', 'loading']) || state.getIn(['site', 'remove', 'loading']), + }), + { + save, + remove, + edit, + update, + pushNewSite, + fetchList, + setSiteId, + } +) @withRouter export default class NewSiteForm extends React.PureComponent { - state = { - existsError: false, - } + state = { + existsError: false, + }; - componentDidMount() { - const { location: { pathname }, match: { params: { siteId } } } = this.props; - if (pathname.includes('onboarding')) { - this.props.setSiteId(siteId); - } - } + componentDidMount() { + const { + location: { pathname }, + match: { + params: { siteId }, + }, + } = this.props; + if (pathname.includes('onboarding')) { + this.props.setSiteId(siteId); + } + } - onSubmit = e => { - e.preventDefault(); - const { site, siteList, location: { pathname } } = this.props; - if (!site.exists() && siteList.some(({ name }) => name === site.name)) { - return this.setState({ existsError: true }); - } - if (site.exists()) { - this.props.update(this.props.site, this.props.site.id).then(() => { - this.props.onClose(null) - this.props.fetchList(); - }) - } else { - this.props.save(this.props.site).then(() => { - this.props.fetchList().then(() => { - const { sites } = this.props; - const site = sites.last(); - if (!pathname.includes('/client')) { - this.props.setSiteId(site.get('id')) - } - this.props.onClose(null, site) - }) - - // this.props.pushNewSite(site) - }); - } - } + onSubmit = (e) => { + e.preventDefault(); + const { + site, + siteList, + location: { pathname }, + } = this.props; + if (!site.exists() && siteList.some(({ name }) => name === site.name)) { + return this.setState({ existsError: true }); + } + if (site.exists()) { + this.props.update(this.props.site, this.props.site.id).then(() => { + this.props.onClose(null); + this.props.fetchList(); + }); + } else { + this.props.save(this.props.site).then(() => { + this.props.fetchList().then(() => { + const { sites } = this.props; + const site = sites.last(); + if (!pathname.includes('/client')) { + this.props.setSiteId(site.get('id')); + } + this.props.onClose(null, site); + }); - remove = async (site) => { - if (await confirm({ - header: 'Projects', - confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.` - })) { - this.props.remove(site.id).then(() => { - this.props.onClose(null) - }); - } - }; + // this.props.pushNewSite(site) + }); + } + }; - edit = ({ target: { name, value } }) => { - this.setState({ existsError: false }); - this.props.edit({ [ name ]: value }); - } + remove = async (site) => { + if ( + await confirm({ + header: 'Projects', + confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`, + }) + ) { + this.props.remove(site.id).then(() => { + this.props.onClose(null); + }); + } + }; - render() { - const { site, loading } = this.props; - return ( -
-
- - - - -
- - {site.exists() && ( - - )} -
- { this.state.existsError && -
- { "Site exists already. Please choose another one." } -
- } -
-
- ); - } -} \ No newline at end of file + edit = ({ target: { name, value } }) => { + this.setState({ existsError: false }); + this.props.edit({ [name]: value }); + }; + + render() { + const { site, loading } = this.props; + return ( +
+

{site.exists() ? 'Edit Project' : 'New Project'}

+
+
+ + + + +
+ + {site.exists() && ( + + )} +
+ {this.state.existsError &&
{'Site exists already. Please choose another one.'}
} +
+
+
+ ); + } +} diff --git a/frontend/app/components/Client/Sites/ProjectKey.tsx b/frontend/app/components/Client/Sites/ProjectKey.tsx new file mode 100644 index 000000000..d53b336f8 --- /dev/null +++ b/frontend/app/components/Client/Sites/ProjectKey.tsx @@ -0,0 +1,8 @@ +import { withCopy } from 'HOCs'; +import React from 'react'; + +function ProjectKey({ value, tooltip }: any) { + return
{value}
; +} + +export default withCopy(ProjectKey); diff --git a/frontend/app/components/Client/Sites/Sites.js b/frontend/app/components/Client/Sites/Sites.js index 1c96c0b3c..ab5f5be25 100644 --- a/frontend/app/components/Client/Sites/Sites.js +++ b/frontend/app/components/Client/Sites/Sites.js @@ -1,18 +1,18 @@ import React from 'react'; import { connect } from 'react-redux'; -import cn from 'classnames'; import withPageTitle from 'HOCs/withPageTitle'; -import { Loader, SlideModal, Icon, Button, Popup, TextLink } from 'UI'; +import { Loader, Button, Popup, TextLink } from 'UI'; import { init, remove, fetchGDPR } from 'Duck/site'; import { RED, YELLOW, GREEN, STATUS_COLOR_MAP } from 'Types/site'; import stl from './sites.module.css'; import NewSiteForm from './NewSiteForm'; -import GDPRForm from './GDPRForm'; -import TrackingCodeModal from 'Shared/TrackingCodeModal'; -import BlockedIps from './BlockedIps'; import { confirm, PageTitle } from 'UI'; import SiteSearch from './SiteSearch'; import AddProjectButton from './AddProjectButton'; +import InstallButton from './InstallButton'; +import ProjectKey from './ProjectKey'; +import { useModal } from 'App/components/Modal'; +import { getInitials } from 'App/utils'; const STATUS_MESSAGE_MAP = { [RED]: ' There seems to be an issue (please verify your installation)', @@ -20,11 +20,7 @@ const STATUS_MESSAGE_MAP = { [GREEN]: 'All good!', }; -const BLOCKED_IPS = 'BLOCKED_IPS'; -const NONE = 'NONE'; - const NEW_SITE_FORM = 'NEW_SITE_FORM'; -const GDPR_FORM = 'GDPR_FORM'; @connect( (state) => ({ @@ -43,20 +39,9 @@ const GDPR_FORM = 'GDPR_FORM'; @withPageTitle('Projects - OpenReplay Preferences') class Sites extends React.PureComponent { state = { - showTrackingCode: false, - modalContent: NONE, - detailContent: NONE, searchQuery: '', }; - toggleBlockedIp = () => { - this.setState({ - detailContent: this.state.detailContent === BLOCKED_IPS ? NONE : BLOCKED_IPS, - }); - }; - - closeModal = () => this.setState({ modalContent: NONE, detailContent: NONE }); - edit = (site) => { this.props.init(site); this.setState({ modalContent: NEW_SITE_FORM }); @@ -73,128 +58,59 @@ class Sites extends React.PureComponent { } }; - showGDPRForm = (site) => { - this.props.init(site); - this.setState({ modalContent: GDPR_FORM }); - }; - - showNewSiteForm = () => { - this.props.init(); - this.setState({ modalContent: NEW_SITE_FORM }); - }; - - showTrackingCode = (site) => { - this.props.init(site); - this.setState({ showTrackingCode: true }); - }; - - getModalTitle() { - switch (this.state.modalContent) { - case NEW_SITE_FORM: - return this.props.site.exists() ? 'Update Project' : 'New Project'; - case GDPR_FORM: - return 'Project Settings'; - default: - return ''; - } - } - - renderModalContent() { - switch (this.state.modalContent) { - case NEW_SITE_FORM: - return ; - case GDPR_FORM: - return ; - default: - return null; - } - } - - renderModalDetailContent() { - switch (this.state.detailContent) { - case BLOCKED_IPS: - return ; - default: - return null; - } - } - render() { - const { loading, sites, site, user, account } = this.props; - const { modalContent, showTrackingCode } = this.state; + const { loading, sites, user } = this.props; const isAdmin = user.admin || user.superAdmin; const filteredSites = sites.filter((site) => site.name.toLowerCase().includes(this.state.searchQuery.toLowerCase())); return ( - this.setState({ showTrackingCode: false })} - site={site} - /> -
- Projects
} - actionButton={} - /> + Projects
} actionButton={} />
- + this.setState({ searchQuery: value })} />
-
Name
+
Project Name
Key
{filteredSites.map((_site) => (
- -
- + +
+
+
+ {getInitials(_site.name)} +
{_site.host}
- {_site.projectKey} +
- +
- + this.props.init(_site)} />
@@ -207,3 +123,12 @@ class Sites extends React.PureComponent { } export default Sites; + +function EditButton({ isAdmin, onClick }) { + const { showModal, hideModal } = useModal(); + const _onClick = () => { + onClick(); + showModal(); + }; + return + + + +
- +
); } -export default UserListItem; \ No newline at end of file +export default UserListItem; diff --git a/frontend/app/components/Client/Webhooks/ListItem.js b/frontend/app/components/Client/Webhooks/ListItem.js index c493cc176..ab640b8e8 100644 --- a/frontend/app/components/Client/Webhooks/ListItem.js +++ b/frontend/app/components/Client/Webhooks/ListItem.js @@ -1,24 +1,20 @@ import React from 'react'; import { Icon } from 'UI'; import styles from './listItem.module.css'; +import { Button } from 'UI'; const ListItem = ({ webhook, onEdit, onDelete }) => { - return ( -
-
- { webhook.name } -
{ webhook.endpoint }
-
-
-
{ e.stopPropagation(); onDelete(webhook) } }> - + return ( +
+
+ {webhook.name} +
{webhook.endpoint}
+
+
+
-
- -
-
-
- ); + ); }; -export default ListItem; \ No newline at end of file +export default ListItem; diff --git a/frontend/app/components/Client/Webhooks/WebhookForm.js b/frontend/app/components/Client/Webhooks/WebhookForm.js index 6ea5737ea..b64a63af8 100644 --- a/frontend/app/components/Client/Webhooks/WebhookForm.js +++ b/frontend/app/components/Client/Webhooks/WebhookForm.js @@ -4,80 +4,91 @@ import { edit, save } from 'Duck/webhook'; import { Form, Button, Input } from 'UI'; import styles from './webhookForm.module.css'; -@connect(state => ({ - webhook: state.getIn(['webhooks', 'instance']), - loading: state.getIn(['webhooks', 'saveRequest', 'loading']), -}), { - edit, - save, -}) +@connect( + (state) => ({ + webhook: state.getIn(['webhooks', 'instance']), + loading: state.getIn(['webhooks', 'saveRequest', 'loading']), + }), + { + edit, + save, + } +) class WebhookForm extends React.PureComponent { - setFocus = () => this.focusElement.focus(); - onChangeSelect = (event, { name, value }) => this.props.edit({ [ name ]: value }); - write = ({ target: { value, name } }) => this.props.edit({ [ name ]: value }); + setFocus = () => this.focusElement.focus(); + onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value }); + write = ({ target: { value, name } }) => this.props.edit({ [name]: value }); - save = () => { - this.props.save(this.props.webhook).then(() => { - this.props.onClose(); - }); - }; + save = () => { + this.props.save(this.props.webhook).then(() => { + this.props.onClose(); + }); + }; - render() { - const { webhook, loading } = this.props; - return ( -
- - - { this.focusElement = ref; } } - name="name" - value={ webhook.name } - onChange={ this.write } - placeholder="Name" - /> - + render() { + const { webhook, loading } = this.props; + return ( +
+

{webhook.exists() ? 'Update' : 'Add'} Webhook

+ + + + { + this.focusElement = ref; + }} + name="name" + value={webhook.name} + onChange={this.write} + placeholder="Name" + /> + - - - { this.focusElement = ref; } } - name="endpoint" - value={ webhook.endpoint } - onChange={ this.write } - placeholder="Endpoint" - /> - + + + { + this.focusElement = ref; + }} + name="endpoint" + value={webhook.endpoint} + onChange={this.write} + placeholder="Endpoint" + /> + - - - { this.focusElement = ref; } } - name="authHeader" - value={ webhook.authHeader } - onChange={ this.write } - placeholder="Auth Header" - /> - + + + { + this.focusElement = ref; + }} + name="authHeader" + value={webhook.authHeader} + onChange={this.write} + placeholder="Auth Header" + /> + - - { webhook.exists() && ( - - )} - - ); - } +
+
+ + {webhook.exists() && } +
+ {webhook.exists() && } +
+ +
+ ); + } } export default WebhookForm; diff --git a/frontend/app/components/Client/Webhooks/Webhooks.js b/frontend/app/components/Client/Webhooks/Webhooks.js index eb5306aa6..076ed0587 100644 --- a/frontend/app/components/Client/Webhooks/Webhooks.js +++ b/frontend/app/components/Client/Webhooks/Webhooks.js @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import cn from 'classnames'; import withPageTitle from 'HOCs/withPageTitle'; -import { IconButton, SlideModal, Loader, NoContent } from 'UI'; +import { Button, Loader, NoContent } from 'UI'; import { init, fetchList, remove } from 'Duck/webhook'; import WebhookForm from './WebhookForm'; import ListItem from './ListItem'; @@ -10,87 +10,74 @@ import styles from './webhooks.module.css'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import { confirm } from 'UI'; import { toast } from 'react-toastify'; +import { useModal } from 'App/components/Modal'; -@connect(state => ({ - webhooks: state.getIn(['webhooks', 'list']), - loading: state.getIn(['webhooks', 'loading']), -}), { - init, - fetchList, - remove, -}) -@withPageTitle('Webhooks - OpenReplay Preferences') -class Webhooks extends React.PureComponent { - state = { showModal: false }; +function Webhooks(props) { + const { webhooks, loading } = props; + const { showModal, hideModal } = useModal(); - componentWillMount() { - this.props.fetchList(); - } + const noSlackWebhooks = webhooks.filter((hook) => hook.type !== 'slack'); + useEffect(() => { + props.fetchList(); + }, []); - closeModal = () => this.setState({ showModal: false }); - init = (v) => { - this.props.init(v); - this.setState({ showModal: true }); - } + const init = (v) => { + props.init(v); + showModal(); + }; - removeWebhook = async (id) => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to remove this webhook?` - })) { - this.props.remove(id).then(() => { - toast.success('Webhook removed successfully'); - }); - } - } + const removeWebhook = async (id) => { + if ( + await confirm({ + header: 'Confirm', + confirmButton: 'Yes, delete', + confirmation: `Are you sure you want to remove this webhook?`, + }) + ) { + props.remove(id).then(() => { + toast.success('Webhook removed successfully'); + }); + hideModal(); + } + }; - render() { - const { webhooks, loading } = this.props; - const { showModal } = this.state; - - const noSlackWebhooks = webhooks.filter(hook => hook.type !== 'slack'); return ( -
- } - onClose={ this.closeModal } - /> -
-

{ 'Webhooks' }

- this.init() } /> -
- - - - -
No webhooks available.
-
- } - size="small" - show={ noSlackWebhooks.size === 0 } - // animatedIcon="no-results" - > -
- { noSlackWebhooks.map(webhook => ( - this.init(webhook) } - onDelete={ () => this.removeWebhook(webhook.webhookId) } - /> - ))} +
+
+

{'Webhooks'}

+
- - -
+ + + + +
No webhooks available.
+
+ } + size="small" + show={noSlackWebhooks.size === 0} + > +
+ {noSlackWebhooks.map((webhook) => ( + init(webhook)} /> + ))} +
+ + +
); - } } -export default Webhooks; \ No newline at end of file +export default connect( + (state) => ({ + webhooks: state.getIn(['webhooks', 'list']), + loading: state.getIn(['webhooks', 'loading']), + }), + { + init, + fetchList, + remove, + } +)(withPageTitle('Webhooks - OpenReplay Preferences')(Webhooks)); diff --git a/frontend/app/components/Client/client.module.css b/frontend/app/components/Client/client.module.css index 8e69458ef..43d311b31 100644 --- a/frontend/app/components/Client/client.module.css +++ b/frontend/app/components/Client/client.module.css @@ -7,7 +7,7 @@ .main { max-height: 100%; display: flex; - min-height: calc(100vh - 81px); + /* min-height: calc(100vh - 81px); */ & .tabMenu { width: 240px; diff --git a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx index 66a4654e3..e3de553d2 100644 --- a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx @@ -1,21 +1,21 @@ -import React, { useEffect, useState } from "react"; -import { NoContent, Loader, Pagination } from "UI"; -import Select from "Shared/Select"; -import cn from "classnames"; -import { useStore } from "App/mstore"; -import SessionItem from "Shared/SessionItem"; -import { observer, useObserver } from "mobx-react-lite"; -import { DateTime } from "luxon"; -import { debounce } from "App/utils"; -import useIsMounted from "App/hooks/useIsMounted"; -import AnimatedSVG, { ICONS } from "Shared/AnimatedSVG/AnimatedSVG"; +import React, { useEffect, useState } from 'react'; +import { NoContent, Loader, Pagination } from 'UI'; +import Select from 'Shared/Select'; +import cn from 'classnames'; +import { useStore } from 'App/mstore'; +import SessionItem from 'Shared/SessionItem'; +import { observer, useObserver } from 'mobx-react-lite'; +import { DateTime } from 'luxon'; +import { debounce } from 'App/utils'; +import useIsMounted from 'App/hooks/useIsMounted'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; interface Props { className?: string; } function WidgetSessions(props: Props) { - const { className = "" } = props; - const [activeSeries, setActiveSeries] = useState("all"); + const { className = '' } = props; + const [activeSeries, setActiveSeries] = useState('all'); const [data, setData] = useState([]); const isMounted = useIsMounted(); const [loading, setLoading] = useState(false); @@ -23,15 +23,9 @@ function WidgetSessions(props: Props) { const { dashboardStore, metricStore } = useStore(); const filter = useObserver(() => dashboardStore.drillDownFilter); const widget: any = useObserver(() => metricStore.instance); - const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat( - "LLL dd, yyyy HH:mm" - ); - const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat( - "LLL dd, yyyy HH:mm" - ); - const [seriesOptions, setSeriesOptions] = useState([ - { label: "All", value: "all" }, - ]); + const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm'); + const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm'); + const [seriesOptions, setSeriesOptions] = useState([{ label: 'All', value: 'all' }]); const writeOption = ({ value }: any) => setActiveSeries(value.value); useEffect(() => { @@ -40,7 +34,7 @@ function WidgetSessions(props: Props) { label: item.seriesName, value: item.seriesId, })); - setSeriesOptions([{ label: "All", value: "all" }, ...seriesOptions]); + setSeriesOptions([{ label: 'All', value: 'all' }, ...seriesOptions]); }, [data]); const fetchSessions = (metricId: any, filter: any) => { @@ -55,10 +49,7 @@ function WidgetSessions(props: Props) { setLoading(false); }); }; - const debounceRequest: any = React.useCallback( - debounce(fetchSessions, 1000), - [] - ); + const debounceRequest: any = React.useCallback(debounce(fetchSessions, 1000), []); const depsString = JSON.stringify(widget.series); useEffect(() => { @@ -68,13 +59,7 @@ function WidgetSessions(props: Props) { page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize, }); - }, [ - filter.startTimestamp, - filter.endTimestamp, - filter.filters, - depsString, - metricStore.sessionsPage, - ]); + }, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage]); return useObserver(() => (
@@ -82,28 +67,15 @@ function WidgetSessions(props: Props) {

Sessions

- between{" "} - - {startTime} - {" "} - and{" "} - - {endTime} - {" "} + between {startTime} and{' '} + {endTime}{' '}
- {widget.metricType !== "table" && ( + {widget.metricType !== 'table' && (
- - Filter by Series - -
)}
@@ -112,14 +84,10 @@ function WidgetSessions(props: Props) { - -
- No recordings found -
+
+ +
+
No relevant sessions found for the selected time period.
} show={filteredSessions.sessions.length === 0} @@ -134,13 +102,8 @@ function WidgetSessions(props: Props) {
- metricStore.updateKey("sessionsPage", page) - } + totalPages={Math.ceil(filteredSessions.total / metricStore.sessionsPageSize)} + onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)} limit={metricStore.sessionsPageSize} debounceRequest={500} /> @@ -155,13 +118,9 @@ function WidgetSessions(props: Props) { const getListSessionsBySeries = (data: any, seriesId: any) => { const arr: any = { sessions: [], total: 0 }; data.forEach((element: any) => { - if (seriesId === "all") { + if (seriesId === 'all') { const sessionIds = arr.sessions.map((i: any) => i.sessionId); - arr.sessions.push( - ...element.sessions.filter( - (i: any) => !sessionIds.includes(i.sessionId) - ) - ); + arr.sessions.push(...element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId))); arr.total = element.total; } else { if (element.seriesId === seriesId) { diff --git a/frontend/app/components/Errors/Error/DistributionBar.js b/frontend/app/components/Errors/Error/DistributionBar.js index 6df611d0a..e6cc38ca5 100644 --- a/frontend/app/components/Errors/Error/DistributionBar.js +++ b/frontend/app/components/Errors/Error/DistributionBar.js @@ -6,52 +6,55 @@ import cls from './distributionBar.module.css'; import { colorScale } from 'App/utils'; function DistributionBar({ className, title, partitions }) { - if (partitions.length === 0) { - return null; - } + if (partitions.length === 0) { + return null; + } - const values = Array(partitions.length).fill().map((element, index) => index + 0); - const colors = colorScale(values, Styles.colors); + const values = Array(partitions.length) + .fill() + .map((element, index) => index + 0); + const colors = colorScale(values, Styles.colors); - return ( -
-
-
{ title }
-
-
- -
-
{ `${ Math.round(partitions[0].prc) }% ` }
-
-
-
- { partitions.map((p, index) => - - { p.label }
- {`${ Math.round(p.prc) }%`} -
- } - className="w-full" - > -
- - )} -
-
- ); + return ( +
+
+
{title}
+
+
+ +
+
{`${Math.round(partitions[0].prc)}% `}
+
+
+
+ {partitions.map((p, index) => ( + + {p.label} +
+ {`${Math.round(p.prc)}%`} +
+ } + style={{ + marginLeft: '1px', + width: `${p.prc}%`, + backgroundColor: colors(index), + }} + > +
+ + ))} +
+
+ ); } -DistributionBar.displayName = "DistributionBar"; -export default DistributionBar; \ No newline at end of file +DistributionBar.displayName = 'DistributionBar'; +export default DistributionBar; diff --git a/frontend/app/components/Header/Header.js b/frontend/app/components/Header/Header.js index 9726e83ee..1b5b97c22 100644 --- a/frontend/app/components/Header/Header.js +++ b/frontend/app/components/Header/Header.js @@ -66,8 +66,8 @@ const Header = (props) => { return (
-
-
+
+
v{window.env.VERSION}
diff --git a/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx b/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx index 695139fa3..9c438c50f 100644 --- a/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx +++ b/frontend/app/components/Header/NewProjectButton/NewProjectButton.tsx @@ -3,26 +3,35 @@ import { Icon } from 'UI'; import cn from 'classnames'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; - -function NewProjectButton({ onClick, isAdmin = false }: any) { +import { useModal } from 'App/components/Modal'; +import NewSiteForm from 'App/components/Client/Sites/NewSiteForm'; +import { init } from 'Duck/site'; +import { connect } from 'react-redux'; +interface Props { + isAdmin?: boolean; + init?: (data: any) => void; +} +function NewProjectButton(props: Props) { + const { isAdmin = false } = props; const { userStore } = useStore(); const limtis = useObserver(() => userStore.limits); const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0)); + const { showModal, hideModal } = useModal(); + + const onClick = () => { + props.init({}); + showModal(, { right: true }); + }; return (
- + > + Add New Project
); } -export default NewProjectButton; \ No newline at end of file +export default connect(null, { init })(NewProjectButton); diff --git a/frontend/app/components/Header/SiteDropdown.js b/frontend/app/components/Header/SiteDropdown.js index 228190111..87fe2a0c2 100644 --- a/frontend/app/components/Header/SiteDropdown.js +++ b/frontend/app/components/Header/SiteDropdown.js @@ -2,104 +2,100 @@ import React from 'react'; import { connect } from 'react-redux'; import { setSiteId } from 'Duck/site'; import { withRouter } from 'react-router-dom'; -import { hasSiteId, siteChangeAvaliable, isRoute } from 'App/routes'; +import { hasSiteId, siteChangeAvaliable } from 'App/routes'; import { STATUS_COLOR_MAP, GREEN } from 'Types/site'; -import { Icon, SlideModal } from 'UI'; -import { pushNewSite } from 'Duck/user' +import { Icon } from 'UI'; +import { pushNewSite } from 'Duck/user'; import { init } from 'Duck/site'; import styles from './siteDropdown.module.css'; import cn from 'classnames'; -import NewSiteForm from '../Client/Sites/NewSiteForm'; import { clearSearch } from 'Duck/search'; import { clearSearch as clearSearchLive } from 'Duck/liveSearch'; import { fetchList as fetchIntegrationVariables } from 'Duck/customField'; -import { withStore } from 'App/mstore' +import { withStore } from 'App/mstore'; import AnimatedSVG, { ICONS } from '../shared/AnimatedSVG/AnimatedSVG'; import NewProjectButton from './NewProjectButton'; @withStore @withRouter -@connect(state => ({ - sites: state.getIn([ 'site', 'list' ]), - siteId: state.getIn([ 'site', 'siteId' ]), - account: state.getIn([ 'user', 'account' ]), -}), { - setSiteId, - pushNewSite, - init, - clearSearch, - clearSearchLive, - fetchIntegrationVariables, -}) +@connect( + (state) => ({ + sites: state.getIn(['site', 'list']), + siteId: state.getIn(['site', 'siteId']), + account: state.getIn(['user', 'account']), + }), + { + setSiteId, + pushNewSite, + init, + clearSearch, + clearSearchLive, + fetchIntegrationVariables, + } +) export default class SiteDropdown extends React.PureComponent { - state = { showProductModal: false } + state = { showProductModal: false }; - closeModal = (e, newSite) => { - this.setState({ showProductModal: false }) - }; + closeModal = (e, newSite) => { + this.setState({ showProductModal: false }); + }; - newSite = () => { - this.props.init({}) - this.setState({showProductModal: true}) - } + newSite = () => { + this.props.init({}); + this.setState({ showProductModal: true }); + }; - switchSite = (siteId) => { - const { mstore, location } = this.props + switchSite = (siteId) => { + const { mstore, location } = this.props; - this.props.setSiteId(siteId); - this.props.fetchIntegrationVariables(); - this.props.clearSearch(location.pathname.includes('/sessions')); - this.props.clearSearchLive(); + this.props.setSiteId(siteId); + this.props.fetchIntegrationVariables(); + this.props.clearSearch(location.pathname.includes('/sessions')); + this.props.clearSearchLive(); - mstore.initClient(); - } + mstore.initClient(); + }; - render() { - const { sites, siteId, account, location: { pathname } } = this.props; - const { showProductModal } = this.state; - const isAdmin = account.admin || account.superAdmin; - const activeSite = sites.find(s => s.id == siteId); - const disabled = !siteChangeAvaliable(pathname); - const showCurrent = hasSiteId(pathname) || siteChangeAvaliable(pathname); - // const canAddSites = isAdmin && account.limits.projects && account.limits.projects.remaining !== 0; + render() { + const { + sites, + siteId, + account, + location: { pathname }, + } = this.props; + const { showProductModal } = this.state; + const isAdmin = account.admin || account.superAdmin; + const activeSite = sites.find((s) => s.id == siteId); + const disabled = !siteChangeAvaliable(pathname); + const showCurrent = hasSiteId(pathname) || siteChangeAvaliable(pathname); + // const canAddSites = isAdmin && account.limits.projects && account.limits.projects.remaining !== 0; - return ( -
- { - showCurrent ? - (activeSite && activeSite.status === GREEN) ? : : - - } -
{ showCurrent && activeSite ? activeSite.host : 'All Projects' }
- -
-
    - { !showCurrent &&
  • { 'Does not require domain selection.' }
  • } - { - sites.map(site => ( -
  • this.switchSite(site.id)}> - - { site.host } -
  • - )) - } -
- -
- - } - onClose={ this.closeModal } - /> -
- ); - } + return ( +
+ {showCurrent ? ( + activeSite && activeSite.status === GREEN ? ( + + ) : ( + + ) + ) : ( + + )} +
{showCurrent && activeSite ? activeSite.host : 'All Projects'}
+ +
+
    + {!showCurrent &&
  • {'Does not require domain selection.'}
  • } + {sites.map((site) => ( +
  • this.switchSite(site.id)}> + + {site.host} +
  • + ))} +
+ +
+
+ ); + } } diff --git a/frontend/app/components/Header/header.module.css b/frontend/app/components/Header/header.module.css index 8eba021a9..9852b7436 100644 --- a/frontend/app/components/Header/header.module.css +++ b/frontend/app/components/Header/header.module.css @@ -9,7 +9,7 @@ $height: 50px; display: flex; justify-content: space-between; border-bottom: solid thin $gray-light; - padding: 0 15px; + /* padding: 0 15px; */ background: $white; z-index: $header; } diff --git a/frontend/app/components/Modal/Modal.tsx b/frontend/app/components/Modal/Modal.tsx index d14f6411a..9dc622a18 100644 --- a/frontend/app/components/Modal/Modal.tsx +++ b/frontend/app/components/Modal/Modal.tsx @@ -3,14 +3,14 @@ import ReactDOM from 'react-dom'; import ModalOverlay from './ModalOverlay'; export default function Modal({ component, props, hideModal }: any) { - return component ? ReactDOM.createPortal( - - {component} - , - document.querySelector("#modal-root"), - ) : <>; -} \ No newline at end of file + return component ? ( + ReactDOM.createPortal( + + {component} + , + document.querySelector('#modal-root') + ) + ) : ( + <> + ); +} diff --git a/frontend/app/components/Modal/ModalOverlay.tsx b/frontend/app/components/Modal/ModalOverlay.tsx index 398e27f2f..5b2a9edab 100644 --- a/frontend/app/components/Modal/ModalOverlay.tsx +++ b/frontend/app/components/Modal/ModalOverlay.tsx @@ -1,18 +1,14 @@ import React from 'react'; -import stl from './ModalOverlay.module.css' +import stl from './ModalOverlay.module.css'; import cn from 'classnames'; function ModalOverlay({ hideModal, children, left = false, right = false }: any) { return (
-
-
{children}
+
+
{children}
); } -export default ModalOverlay; \ No newline at end of file +export default ModalOverlay; diff --git a/frontend/app/components/Modal/index.tsx b/frontend/app/components/Modal/index.tsx index 04e2acd91..920cb2d14 100644 --- a/frontend/app/components/Modal/index.tsx +++ b/frontend/app/components/Modal/index.tsx @@ -3,60 +3,59 @@ import React, { Component, createContext } from 'react'; import Modal from './Modal'; const ModalContext = createContext({ - component: null, - props: { - right: false, - onClose: () => {}, - }, - showModal: (component: any, props: any) => {}, - hideModal: () => {} + component: null, + props: { + right: true, + onClose: () => {}, + }, + showModal: (component: any, props: any) => {}, + hideModal: () => {}, }); export class ModalProvider extends Component { - - handleKeyDown = (e: any) => { - if (e.keyCode === 27) { - this.hideModal(); - } - } - - showModal = (component, props = { }) => { - this.setState({ - component, - props - }); - document.addEventListener('keydown', this.handleKeyDown); - document.querySelector("body").style.overflow = 'hidden'; - }; - - hideModal = () => { - document.removeEventListener('keydown', this.handleKeyDown); - document.querySelector("body").style.overflow = 'visible'; - const { props } = this.state; - if (props.onClose) { - props.onClose(); + handleKeyDown = (e: any) => { + if (e.keyCode === 27) { + this.hideModal(); + } }; - this.setState({ - component: null, - props: {} - }); - } - state = { - component: null, - props: {}, - showModal: this.showModal, - hideModal: this.hideModal - }; + showModal = (component, props = { right: true }) => { + this.setState({ + component, + props, + }); + document.addEventListener('keydown', this.handleKeyDown); + document.querySelector('body').style.overflow = 'hidden'; + }; - render() { - return ( - - - {this.props.children} - - ); - } + hideModal = () => { + document.removeEventListener('keydown', this.handleKeyDown); + document.querySelector('body').style.overflow = 'visible'; + const { props } = this.state; + if (props.onClose) { + props.onClose(); + } + this.setState({ + component: null, + props: {}, + }); + }; + + state = { + component: null, + props: {}, + showModal: this.showModal, + hideModal: this.hideModal, + }; + + render() { + return ( + + + {this.props.children} + + ); + } } export const ModalConsumer = ModalContext.Consumer; diff --git a/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js b/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js index df05ca807..db679f220 100644 --- a/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js +++ b/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js @@ -17,17 +17,17 @@ function IntegrationsTab() {

🔌 -
Plugins
+
Integrations

- + -
+ {/*

🔌
Integrations

- + */} {/*
How are you handling store management?
diff --git a/frontend/app/components/Overview/Overview.tsx b/frontend/app/components/Overview/Overview.tsx new file mode 100644 index 000000000..78b4bfe2b --- /dev/null +++ b/frontend/app/components/Overview/Overview.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import withPageTitle from 'HOCs/withPageTitle'; +import NoSessionsMessage from 'Shared/NoSessionsMessage'; +import MainSearchBar from 'Shared/MainSearchBar'; +import SessionSearch from 'Shared/SessionSearch'; +import SessionListContainer from 'Shared/SessionListContainer/SessionListContainer'; + +function Overview() { + return ( +
+
+
+ + +
+ + + +
+ +
+
+
+
+ ); +} + +export default withPageTitle('Sessions - OpenReplay')(Overview); diff --git a/frontend/app/components/Overview/index.ts b/frontend/app/components/Overview/index.ts new file mode 100644 index 000000000..44bcc2216 --- /dev/null +++ b/frontend/app/components/Overview/index.ts @@ -0,0 +1 @@ +export { default } from './Overview'; \ No newline at end of file diff --git a/frontend/app/components/Session/LivePlayer.js b/frontend/app/components/Session/LivePlayer.js index 5793e2d52..c142167f0 100644 --- a/frontend/app/components/Session/LivePlayer.js +++ b/frontend/app/components/Session/LivePlayer.js @@ -16,13 +16,20 @@ import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; import PlayerBlock from '../Session_/PlayerBlock'; import styles from '../Session_/session.module.css'; - const InitLoader = connectPlayer(state => ({ loading: !state.initialized }))(Loader); - -function LivePlayer ({ session, toggleFullscreen, closeBottomBlock, fullscreen, jwt, loadingCredentials, assistCredendials, request, isEnterprise, hasErrors }) { +function LivePlayer ({ + session, + toggleFullscreen, + closeBottomBlock, + fullscreen, + loadingCredentials, + assistCredendials, + request, + isEnterprise, +}) { useEffect(() => { if (!loadingCredentials) { initPlayer(session, assistCredendials, true); @@ -47,11 +54,10 @@ function LivePlayer ({ session, toggleFullscreen, closeBottomBlock, fullscreen, } const [activeTab, setActiveTab] = useState(''); - return ( - +
@@ -62,19 +68,17 @@ function LivePlayer ({ session, toggleFullscreen, closeBottomBlock, fullscreen, export default withRequest({ initialData: null, - endpoint: '/assist/credentials', - dataWrapper: data => data, - dataName: 'assistCredendials', + endpoint: '/assist/credentials', + dataWrapper: data => data, + dataName: 'assistCredendials', loadingName: 'loadingCredentials', })(withPermissions(['ASSIST_LIVE'], '', true)(connect( state => { return { session: state.getIn([ 'sessions', 'current' ]), showAssist: state.getIn([ 'sessions', 'showChatWindow' ]), - jwt: state.get('jwt'), fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]), isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee', - hasErrors: !!state.getIn([ 'sessions', 'errors' ]), } }, { toggleFullscreen, closeBottomBlock }, diff --git a/frontend/app/components/Session/Session.js b/frontend/app/components/Session/Session.js index 2d9bfa882..d6bf31a53 100644 --- a/frontend/app/components/Session/Session.js +++ b/frontend/app/components/Session/Session.js @@ -15,10 +15,10 @@ const SESSIONS_ROUTE = sessionsRoute(); function Session({ sessionId, loading, - hasErrors, + hasErrors, session, fetchSession, - fetchSlackList, + fetchSlackList, }) { usePageTitle("OpenReplay Session Player"); const [ initializing, setInitializing ] = useState(true) @@ -63,4 +63,4 @@ export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state, pro }, { fetchSession, fetchSlackList, -})(Session)); \ No newline at end of file +})(Session)); diff --git a/frontend/app/components/Session_/Console/ConsoleContent.js b/frontend/app/components/Session_/Console/ConsoleContent.js index a2c084abd..29820de2e 100644 --- a/frontend/app/components/Session_/Console/ConsoleContent.js +++ b/frontend/app/components/Session_/Console/ConsoleContent.js @@ -86,7 +86,13 @@ export default class ConsoleContent extends React.PureComponent { /> - + + + No {activeTab === ALL ? 'Data' : activeTab.toLowerCase()}
} + size="small" + show={filtered.length === 0} + > {filtered.map((l, index) => (
({ - currentSessionId: state.getIn([ 'sessions', 'current', 'sessionId' ]) +@connect((state) => ({ + currentSessionId: state.getIn(['sessions', 'current', 'sessionId']), })) class SessionList extends React.PureComponent { - render() { - const { - similarSessions, - loading, - currentSessionId, - } = this.props; + render() { + const { similarSessions, loading, currentSessionId } = this.props; - const similarSessionWithoutCurrent = similarSessions.map(({sessions, ...rest}) => { - return { - ...rest, - sessions: sessions.map(Session).filter(({ sessionId }) => sessionId !== currentSessionId) - } - }).filter(site => site.sessions.length > 0); - - return ( - - -
- { similarSessionWithoutCurrent.map(site => ( -
-
- - { site.name } -
-
- { site.sessions.map(session => ( -
- + const similarSessionWithoutCurrent = similarSessions + .map(({ sessions, ...rest }) => { + return { + ...rest, + sessions: sessions.map(Session).filter(({ sessionId }) => sessionId !== currentSessionId), + }; + }) + .filter((site) => site.sessions.length > 0); + + return ( + + + +
+
No sessions found.
+
+ } + > +
+ {similarSessionWithoutCurrent.map((site) => ( +
+
+ + {site.name} +
+
+ {site.sessions.map((session) => ( +
+ +
+ ))} +
+
+ ))}
- )) } -
-
- )) } -
- - - ); - } + + + ); + } } export default SessionList; diff --git a/frontend/app/components/Session_/Fetch/Fetch.js b/frontend/app/components/Session_/Fetch/Fetch.js index 1b62d88c8..15114c8c5 100644 --- a/frontend/app/components/Session_/Fetch/Fetch.js +++ b/frontend/app/components/Session_/Fetch/Fetch.js @@ -1,6 +1,6 @@ import React from 'react'; import { getRE } from 'App/utils'; -import { Label, NoContent, Input, SlideModal, CloseButton } from 'UI'; +import { Label, NoContent, Input, SlideModal, CloseButton, Icon } from 'UI'; import { connectPlayer, pause, jump } from 'Player'; // import Autoscroll from '../Autoscroll'; import BottomBlock from '../BottomBlock'; @@ -129,7 +129,17 @@ export default class Fetch extends React.PureComponent {
- + + + No Data +
+ } + // size="small" + show={filteredList.length === 0} + > + {/* */} {[ { diff --git a/frontend/app/components/Session_/Network/NetworkContent.js b/frontend/app/components/Session_/Network/NetworkContent.js index 8e0183324..b6c54b5b4 100644 --- a/frontend/app/components/Session_/Network/NetworkContent.js +++ b/frontend/app/components/Session_/Network/NetworkContent.js @@ -1,7 +1,7 @@ import React from 'react'; import cn from 'classnames'; // import { connectPlayer } from 'Player'; -import { QuestionMarkHint, Popup, Tabs, Input } from 'UI'; +import { QuestionMarkHint, Popup, Tabs, Input, NoContent, Icon } from 'UI'; import { getRE } from 'App/utils'; import { TYPES } from 'Types/session/resource'; import { formatBytes } from 'App/utils'; @@ -21,270 +21,261 @@ const MEDIA = 'media'; const OTHER = 'other'; const TAB_TO_TYPE_MAP = { - [ XHR ]: TYPES.XHR, - [ JS ]: TYPES.JS, - [ CSS ]: TYPES.CSS, - [ IMG ]: TYPES.IMG, - [ MEDIA ]: TYPES.MEDIA, - [ OTHER ]: TYPES.OTHER -} -const TABS = [ ALL, XHR, JS, CSS, IMG, MEDIA, OTHER ].map(tab => ({ - text: tab, - key: tab, + [XHR]: TYPES.XHR, + [JS]: TYPES.JS, + [CSS]: TYPES.CSS, + [IMG]: TYPES.IMG, + [MEDIA]: TYPES.MEDIA, + [OTHER]: TYPES.OTHER, +}; +const TABS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER].map((tab) => ({ + text: tab, + key: tab, })); -const DOM_LOADED_TIME_COLOR = "teal"; -const LOAD_TIME_COLOR = "red"; +const DOM_LOADED_TIME_COLOR = 'teal'; +const LOAD_TIME_COLOR = 'red'; -export function renderType(r) { - return ( - { r.type }
} > -
{ r.type }
- - ); +export function renderType(r) { + return ( + {r.type}
}> +
{r.type}
+ + ); } -export function renderName(r) { - return ( - { r.url }
} > -
{ r.name }
- - ); +export function renderName(r) { + return ( + {r.url}
}> +
{r.name}
+ + ); } const renderXHRText = () => ( - - {XHR} - - Use our Fetch plugin - {' to capture HTTP requests and responses, including status codes and bodies.'}
- We also provide support for GraphQL - {' for easy debugging of your queries.'} - - } - className="ml-1" - /> -
+ + {XHR} + + Use our{' '} + + Fetch plugin + + {' to capture HTTP requests and responses, including status codes and bodies.'}
+ We also provide{' '} + + support for GraphQL + + {' for easy debugging of your queries.'} + + } + className="ml-1" + /> +
); function renderSize(r) { - if (r.responseBodySize) return formatBytes(r.responseBodySize); - let triggerText; - let content; - if (r.decodedBodySize == null) { - triggerText = "x"; - content = "Not captured"; - } else { - const headerSize = r.headerSize || 0; - const encodedSize = r.encodedBodySize || 0; - const transferred = headerSize + encodedSize; - const showTransferred = r.headerSize != null; + if (r.responseBodySize) return formatBytes(r.responseBodySize); + let triggerText; + let content; + if (r.decodedBodySize == null) { + triggerText = 'x'; + content = 'Not captured'; + } else { + const headerSize = r.headerSize || 0; + const encodedSize = r.encodedBodySize || 0; + const transferred = headerSize + encodedSize; + const showTransferred = r.headerSize != null; - triggerText = formatBytes(r.decodedBodySize); - content = ( -
    - { showTransferred && -
  • {`${formatBytes( r.encodedBodySize + headerSize )} transfered over network`}
  • - } -
  • {`Resource size: ${formatBytes(r.decodedBodySize)} `}
  • -
+ triggerText = formatBytes(r.decodedBodySize); + content = ( +
    + {showTransferred &&
  • {`${formatBytes(r.encodedBodySize + headerSize)} transfered over network`}
  • } +
  • {`Resource size: ${formatBytes(r.decodedBodySize)} `}
  • +
+ ); + } + + return ( + +
{triggerText}
+
); - } - - return ( - -
{ triggerText }
-
- ); } export function renderDuration(r) { - if (!r.success) return 'x'; + if (!r.success) return 'x'; - const text = `${ Math.floor(r.duration) }ms`; - if (!r.isRed() && !r.isYellow()) return text; + const text = `${Math.floor(r.duration)}ms`; + if (!r.isRed() && !r.isYellow()) return text; - let tooltipText; - let className = "w-full h-full flex items-center "; - if (r.isYellow()) { - tooltipText = "Slower than average"; - className += "warn color-orange"; - } else { - tooltipText = "Much slower than average"; - className += "error color-red"; - } + let tooltipText; + let className = 'w-full h-full flex items-center '; + if (r.isYellow()) { + tooltipText = 'Slower than average'; + className += 'warn color-orange'; + } else { + tooltipText = 'Much slower than average'; + className += 'error color-red'; + } - return ( - -
{ text }
-
- ); + return ( + +
{text}
+
+ ); } export default class NetworkContent extends React.PureComponent { - state = { - filter: '', - activeTab: ALL, - } + state = { + filter: '', + activeTab: ALL, + }; - onTabClick = activeTab => this.setState({ activeTab }) - onFilterChange = ({ target: { value } }) => this.setState({ filter: value }) + onTabClick = (activeTab) => this.setState({ activeTab }); + onFilterChange = ({ target: { value } }) => this.setState({ filter: value }); - render() { - const { - location, - resources, - domContentLoadedTime, - loadTime, - domBuildingTime, - fetchPresented, - onRowClick, - isResult = false, - additionalHeight = 0, - resourcesSize, - transferredSize, - time, - currentIndex - } = this.props; - const { filter, activeTab } = this.state; - const filterRE = getRE(filter, 'i'); - let filtered = resources.filter(({ type, name }) => - filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[ activeTab ])); - const lastIndex = currentIndex || filtered.filter(item => item.time <= time).length - 1; + render() { + const { + location, + resources, + domContentLoadedTime, + loadTime, + domBuildingTime, + fetchPresented, + onRowClick, + isResult = false, + additionalHeight = 0, + resourcesSize, + transferredSize, + time, + currentIndex, + } = this.props; + const { filter, activeTab } = this.state; + const filterRE = getRE(filter, 'i'); + let filtered = resources.filter(({ type, name }) => filterRE.test(name) && (activeTab === ALL || type === TAB_TO_TYPE_MAP[activeTab])); + const lastIndex = currentIndex || filtered.filter((item) => item.time <= time).length - 1; - const referenceLines = []; - if (domContentLoadedTime != null) { - referenceLines.push({ - time: domContentLoadedTime.time, - color: DOM_LOADED_TIME_COLOR, - }) + const referenceLines = []; + if (domContentLoadedTime != null) { + referenceLines.push({ + time: domContentLoadedTime.time, + color: DOM_LOADED_TIME_COLOR, + }); + } + if (loadTime != null) { + referenceLines.push({ + time: loadTime.time, + color: LOAD_TIME_COLOR, + }); + } + + let tabs = TABS; + if (!fetchPresented) { + tabs = TABS.map((tab) => + !isResult && tab.key === XHR + ? { + text: renderXHRText(), + key: XHR, + } + : tab + ); + } + + // const resourcesSize = filtered.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0); + // const transferredSize = filtered + // .reduce((sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), 0); + + return ( + + + +
+ Network + +
+ +
+ + + + 0} /> + 0} /> + + + + + + + No Data +
+ } + size="small" + show={filtered.length === 0} + > + + {[ + { + label: 'Status', + dataKey: 'status', + width: 70, + }, + { + label: 'Type', + dataKey: 'type', + width: 90, + render: renderType, + }, + { + label: 'Name', + width: 200, + render: renderName, + }, + { + label: 'Size', + width: 60, + render: renderSize, + }, + { + label: 'Time', + width: 80, + render: renderDuration, + }, + ]} + + + + + + ); } - if (loadTime != null) { - referenceLines.push({ - time: loadTime.time, - color: LOAD_TIME_COLOR, - }) - } - - let tabs = TABS; - if (!fetchPresented) { - tabs = TABS.map(tab => !isResult && tab.key === XHR - ? { - text: renderXHRText(), - key: XHR, - } - : tab - ); - } - - // const resourcesSize = filtered.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0); - // const transferredSize = filtered - // .reduce((sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), 0); - - return ( - - - -
- Network - -
- -
- - {/*
*/} - {/* */} - {/*
{ location }
*/} - {/*
*/} - {/*
*/} - - - 0 } - /> - 0 } - /> - - - - - - {[ - { - label: "Status", - dataKey: 'status', - width: 70, - }, { - label: "Type", - dataKey: 'type', - width: 90, - render: renderType, - }, { - label: "Name", - width: 200, - render: renderName, - }, - { - label: "Size", - width: 60, - render: renderSize, - }, - { - label: "Time", - width: 80, - render: renderDuration, - } - ]} - -
-
-
- ); - } } diff --git a/frontend/app/components/Session_/Player/Controls/Circle.tsx b/frontend/app/components/Session_/Player/Controls/Circle.tsx index 274b38f8a..73e1e1bb1 100644 --- a/frontend/app/components/Session_/Player/Controls/Circle.tsx +++ b/frontend/app/components/Session_/Player/Controls/Circle.tsx @@ -1,16 +1,18 @@ import React, { memo, FC } from 'react'; import styles from './timeline.module.css'; +import cn from 'classnames'; interface Props { preview?: boolean; + isGreen?: boolean; } -export const Circle: FC = memo(function Box({ preview }) { +export const Circle: FC = memo(function Box({ preview, isGreen }) { return (
) }) -export default Circle; \ No newline at end of file +export default Circle; diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index 0aa33283d..e2197e6b3 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -8,6 +8,11 @@ import { selectStorageListNow, } from 'Player/store'; import LiveTag from 'Shared/LiveTag'; +import { session as sessionRoute, withSiteId } from 'App/routes'; +import { + toggleTimetravel, + jumpToLive, +} from 'Player'; import { Icon } from 'UI'; import { toggleInspectorMode } from 'Player'; @@ -27,9 +32,10 @@ import { EXCEPTIONS, INSPECTOR, } from 'Duck/components/player'; -import { ReduxTime } from './Time'; +import { ReduxTime, AssistDuration } from './Time'; import Timeline from './Timeline'; import ControlButton from './ControlButton'; +import PlayerControls from './components/PlayerControls' import styles from './controls.module.css'; import { Tooltip } from 'react-tippy'; @@ -80,7 +86,6 @@ function getStorageName(type) { fullscreenDisabled: state.messagesLoading, logCount: state.logListNow.length, logRedCount: state.logRedCountNow, - // resourceCount: state.resourceCountNow, resourceRedCount: state.resourceRedCountNow, fetchRedCount: state.fetchRedCountNow, showStack: state.stackList.length > 0, @@ -98,6 +103,7 @@ function getStorageName(type) { exceptionsCount: state.exceptionsListNow.length, showExceptions: state.exceptionsList.length > 0, showLongtasks: state.longtasksList.length > 0, + liveTimeTravel: state.liveTimeTravel, })) @connect((state, props) => { const permissions = state.getIn([ 'user', 'account', 'permissions' ]) || []; @@ -130,7 +136,6 @@ export default class Controls extends React.Component { if ( nextProps.fullscreen !== this.props.fullscreen || nextProps.bottomBlock !== this.props.bottomBlock || - nextProps.endTime !== this.props.endTime || nextProps.live !== this.props.live || nextProps.livePlay !== this.props.livePlay || nextProps.playing !== this.props.playing || @@ -159,7 +164,8 @@ export default class Controls extends React.Component { nextProps.graphqlCount !== this.props.graphqlCount || nextProps.showExceptions !== this.props.showExceptions || nextProps.exceptionsCount !== this.props.exceptionsCount || - nextProps.showLongtasks !== this.props.showLongtasks + nextProps.showLongtasks !== this.props.showLongtasks || + nextProps.liveTimeTravel !== this.props.liveTimeTravel ) return true; return false; } @@ -207,7 +213,7 @@ export default class Controls extends React.Component { goLive =() => this.props.jump(this.props.endTime) renderPlayBtn = () => { - const { completed, playing, disabled } = this.props; + const { completed, playing } = this.props; let label; let icon; if (completed) { @@ -280,6 +286,9 @@ export default class Controls extends React.Component { fullscreen, inspectorMode, closedLive, + toggleSpeed, + toggleSkip, + liveTimeTravel, } = this.props; const toggleBottomTools = (blockName) => { @@ -291,75 +300,38 @@ export default class Controls extends React.Component { toggleBottomBlock(blockName); } } + return ( -
- { !live && } +
+ { !live || liveTimeTravel ? : null} { !fullscreen && -
+
- { !live && ( -
- { this.renderPlayBtn() } - { !live && ( -
- - / - -
- )} - -
- - {this.controlIcon("skip-forward-fill", 18, this.backTenSeconds, true, 'hover:bg-active-blue-border color-main h-full flex items-center')} - -
10s
- - {this.controlIcon("skip-forward-fill", 18, this.forthTenSeconds, false, 'hover:bg-active-blue-border color-main h-full flex items-center')} - -
- - {!live && -
- - - - - -
- } -
+ {!live && ( + )} { live && !closedLive && (
- - {'Elapsed'} - + livePlay ? null : jumpToLive()} /> +
+ + {!liveTimeTravel && ( +
+ See Past Activity +
+ )}
)}
diff --git a/frontend/app/components/Session_/Player/Controls/CustomDragLayer.tsx b/frontend/app/components/Session_/Player/Controls/CustomDragLayer.tsx index c72f03ce2..200c1c79f 100644 --- a/frontend/app/components/Session_/Player/Controls/CustomDragLayer.tsx +++ b/frontend/app/components/Session_/Player/Controls/CustomDragLayer.tsx @@ -95,4 +95,4 @@ const CustomDragLayer: FC = memo(function CustomDragLayer(props) { ); }) -export default CustomDragLayer; \ No newline at end of file +export default CustomDragLayer; diff --git a/frontend/app/components/Session_/Player/Controls/DraggableCircle.tsx b/frontend/app/components/Session_/Player/Controls/DraggableCircle.tsx index 385707879..0e1ee85a7 100644 --- a/frontend/app/components/Session_/Player/Controls/DraggableCircle.tsx +++ b/frontend/app/components/Session_/Player/Controls/DraggableCircle.tsx @@ -9,10 +9,12 @@ function getStyles( isDragging: boolean, ): CSSProperties { // const transform = `translate3d(${(left * 1161) / 100}px, -8px, 0)` + const leftPosition = left > 100 ? 100 : left + return { position: 'absolute', top: '-3px', - left: `${left}%`, + left: `${leftPosition}%`, // transform, // WebkitTransform: transform, // IE fallback: hide the real node using CSS when dragging @@ -59,9 +61,9 @@ const DraggableCircle: FC = memo(function DraggableCircle(props) { style={getStyles(left, isDragging)} role="DraggableBox" > - + 99} />
); }) -export default DraggableCircle \ No newline at end of file +export default DraggableCircle diff --git a/frontend/app/components/Session_/Player/Controls/Time.js b/frontend/app/components/Session_/Player/Controls/Time.js index b0e95c6f0..ca3c6ce4c 100644 --- a/frontend/app/components/Session_/Player/Controls/Time.js +++ b/frontend/app/components/Session_/Player/Controls/Time.js @@ -2,6 +2,7 @@ import React from 'react'; import { Duration } from 'luxon'; import { connectPlayer } from 'Player'; import styles from './time.module.css'; +import { Tooltip } from 'react-tippy'; const Time = ({ time, isCustom, format = 'm:ss', }) => (
@@ -11,13 +12,37 @@ const Time = ({ time, isCustom, format = 'm:ss', }) => ( Time.displayName = "Time"; - const ReduxTime = connectPlayer((state, { name, format }) => ({ time: state[ name ], format, }))(Time); +const AssistDurationCont = connectPlayer( + state => { + const assistStart = state.assistStart; + return { + assistStart, + } + } +)(({ assistStart }) => { + const [assistDuration, setAssistDuration] = React.useState('00:00'); + React.useEffect(() => { + const interval = setInterval(() => { + setAssistDuration(Duration.fromMillis(+new Date() - assistStart).toFormat('mm:ss')); + } + , 1000); + return () => clearInterval(interval); + }, []) + return ( + <> + Elapsed {assistDuration} + + ) +}) + +const AssistDuration = React.memo(AssistDurationCont); + ReduxTime.displayName = "ReduxTime"; -export default Time; -export { ReduxTime }; +export default React.memo(Time); +export { ReduxTime, AssistDuration }; diff --git a/frontend/app/components/Session_/Player/Controls/TimeTooltip.tsx b/frontend/app/components/Session_/Player/Controls/TimeTooltip.tsx new file mode 100644 index 000000000..fe22c4ea9 --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/TimeTooltip.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +// @ts-ignore +import { Duration } from 'luxon'; +import { connect } from 'react-redux'; +// @ts-ignore +import stl from './timeline.module.css'; + +function TimeTooltip({ time, offset, isVisible, liveTimeTravel }: { time: number; offset: number; isVisible: boolean, liveTimeTravel: boolean }) { + const duration = Duration.fromMillis(time).toFormat(`${liveTimeTravel ? '-' : ''}mm:ss`); + return ( +
+ {!time ? 'Loading' : duration} +
+ ); +} + +export default connect((state) => { + const { time = 0, offset = 0, isVisible } = state.getIn(['sessions', 'timeLineTooltip']); + return { time, offset, isVisible }; +})(TimeTooltip); diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index e6879671d..c177c14e0 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -1,210 +1,256 @@ import React from 'react'; import { connect } from 'react-redux'; -// import cn from 'classnames'; +import cn from 'classnames'; import { connectPlayer, Controls } from 'Player'; -// import { TimelinePointer, Icon } from 'UI'; +import { TimelinePointer, Icon } from 'UI'; import TimeTracker from './TimeTracker'; import stl from './timeline.module.css'; import { TYPES } from 'Types/session/event'; -import { setTimelinePointer } from 'Duck/sessions'; +import { setTimelinePointer, setTimelineHoverTime } from 'Duck/sessions'; import DraggableCircle from './DraggableCircle'; import CustomDragLayer from './CustomDragLayer'; import { debounce } from 'App/utils'; -// import { Tooltip } from 'react-tippy'; +import { Tooltip } from 'react-tippy'; +import TooltipContainer from './components/TooltipContainer'; -const BOUNDRY = 0; +const BOUNDRY = 15 function getTimelinePosition(value, scale) { - const pos = value * scale; + const pos = value * scale; - return pos > 100 ? 100 : pos; + return pos > 100 ? 100 : pos; } -// const getPointerIcon = (type) => { -// // exception, -// switch (type) { -// case 'fetch': -// return 'funnel/file-earmark-minus-fill'; -// case 'exception': -// return 'funnel/exclamation-circle-fill'; -// case 'log': -// return 'funnel/exclamation-circle-fill'; -// case 'stack': -// return 'funnel/patch-exclamation-fill'; -// case 'resource': -// return 'funnel/file-earmark-minus-fill'; +const getPointerIcon = (type) => { + // exception, + switch(type) { + case 'fetch': + return 'funnel/file-earmark-minus-fill'; + case 'exception': + return 'funnel/exclamation-circle-fill'; + case 'log': + return 'funnel/exclamation-circle-fill'; + case 'stack': + return 'funnel/patch-exclamation-fill'; + case 'resource': + return 'funnel/file-earmark-minus-fill'; -// case 'dead_click': -// return 'funnel/dizzy'; -// case 'click_rage': -// return 'funnel/dizzy'; -// case 'excessive_scrolling': -// return 'funnel/mouse'; -// case 'bad_request': -// return 'funnel/file-medical-alt'; -// case 'missing_resource': -// return 'funnel/file-earmark-minus-fill'; -// case 'memory': -// return 'funnel/sd-card'; -// case 'cpu': -// return 'funnel/microchip'; -// case 'slow_resource': -// return 'funnel/hourglass-top'; -// case 'slow_page_load': -// return 'funnel/hourglass-top'; -// case 'crash': -// return 'funnel/file-exclamation'; -// case 'js_exception': -// return 'funnel/exclamation-circle-fill'; -// } + case 'dead_click': + return 'funnel/dizzy'; + case 'click_rage': + return 'funnel/dizzy'; + case 'excessive_scrolling': + return 'funnel/mouse'; + case 'bad_request': + return 'funnel/file-medical-alt'; + case 'missing_resource': + return 'funnel/file-earmark-minus-fill'; + case 'memory': + return 'funnel/sd-card'; + case 'cpu': + return 'funnel/microchip'; + case 'slow_resource': + return 'funnel/hourglass-top'; + case 'slow_page_load': + return 'funnel/hourglass-top'; + case 'crash': + return 'funnel/file-exclamation'; + case 'js_exception': + return 'funnel/exclamation-circle-fill'; + } + + return 'info'; +} -// return 'info'; -// }; let deboucneJump = () => null; -@connectPlayer((state) => ({ - playing: state.playing, - time: state.time, - skipIntervals: state.skipIntervals, - events: state.eventList, - skip: state.skip, - // not updating properly rn - // skipToIssue: state.skipToIssue, - disabled: state.cssLoading || state.messagesLoading || state.markedTargets, - endTime: state.endTime, - live: state.live, - // logList: state.logList, - // exceptionsList: state.exceptionsList, - // resourceList: state.resourceList, - // stackList: state.stackList, - // fetchList: state.fetchList, +let debounceTooltipChange = () => null; +@connectPlayer(state => ({ + playing: state.playing, + time: state.time, + skipIntervals: state.skipIntervals, + events: state.eventList, + skip: state.skip, + // not updating properly rn + // skipToIssue: state.skipToIssue, + disabled: state.cssLoading || state.messagesLoading || state.markedTargets, + endTime: state.endTime, + live: state.live, + logList: state.logList, + exceptionsList: state.exceptionsList, + resourceList: state.resourceList, + stackList: state.stackList, + fetchList: state.fetchList, })) -@connect( - (state) => ({ - issues: state.getIn(['sessions', 'current', 'issues']), - clickRageTime: state.getIn(['sessions', 'current', 'clickRage']) && state.getIn(['sessions', 'current', 'clickRageTime']), - returningLocationTime: - state.getIn(['sessions', 'current', 'returningLocation']) && state.getIn(['sessions', 'current', 'returningLocationTime']), - }), - { setTimelinePointer } -) +@connect(state => ({ + issues: state.getIn([ 'sessions', 'current', 'issues' ]), + clickRageTime: state.getIn([ 'sessions', 'current', 'clickRage' ]) && + state.getIn([ 'sessions', 'current', 'clickRageTime' ]), + returningLocationTime: state.getIn([ 'sessions', 'current', 'returningLocation' ]) && + state.getIn([ 'sessions', 'current', 'returningLocationTime' ]), + tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']) +}), { setTimelinePointer, setTimelineHoverTime }) export default class Timeline extends React.PureComponent { - progressRef = React.createRef(); - wasPlaying = false; + progressRef = React.createRef() + timelineRef = React.createRef() + wasPlaying = false - seekProgress = (e) => { - const { endTime } = this.props; - const p = e.nativeEvent.offsetX / e.target.offsetWidth; - const time = Math.max(Math.round(p * endTime), 0); - this.props.jump(time); - }; + seekProgress = (e) => { + const time = this.getTime(e) + this.props.jump(time); + this.hideTimeTooltip() + } - createEventClickHandler = (pointer) => (e) => { - e.stopPropagation(); - this.props.jump(pointer.time); - this.props.setTimelinePointer(pointer); - }; + getTime = (e) => { + const { endTime } = this.props; + const p = e.nativeEvent.offsetX / e.target.offsetWidth; + const time = Math.max(Math.round(p * endTime), 0); - componentDidMount() { - const { issues } = this.props; - const skipToIssue = Controls.updateSkipToIssue(); - const firstIssue = issues.get(0); - deboucneJump = debounce(this.props.jump, 500); + return time + } - if (firstIssue && skipToIssue) { - this.props.jump(firstIssue.time); - } + createEventClickHandler = pointer => (e) => { + e.stopPropagation(); + this.props.jump(pointer.time); + this.props.setTimelinePointer(pointer); + } + + componentDidMount() { + const { issues } = this.props; + const skipToIssue = Controls.updateSkipToIssue(); + const firstIssue = issues.get(0); + deboucneJump = debounce(this.props.jump, 500); + debounceTooltipChange = debounce(this.props.setTimelineHoverTime, 50); + + if (firstIssue && skipToIssue) { + this.props.jump(firstIssue.time); } + } - onDragEnd = () => { - if (this.wasPlaying) { - this.props.togglePlay(); - } - }; + onDragEnd = () => { + if (this.wasPlaying) { + this.props.togglePlay(); + } + } - onDrag = (offset) => { - const { endTime } = this.props; + onDrag = (offset) => { + const { endTime } = this.props; - const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth; - const time = Math.max(Math.round(p * endTime), 0); - deboucneJump(time); - if (this.props.playing) { - this.wasPlaying = true; - this.props.pause(); - } - }; + const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth; + const time = Math.max(Math.round(p * endTime), 0); + deboucneJump(time); + this.hideTimeTooltip(); + if (this.props.playing) { + this.wasPlaying = true; + this.props.pause(); + } + } - render() { - const { - events, - skip, - skipIntervals, - disabled, - endTime, - // live, - // logList, - // exceptionsList, - // resourceList, - // clickRageTime, - // stackList, - // fetchList, - // issues, - } = this.props; + showTimeTooltip = (e) => { + if (e.target !== this.progressRef.current && e.target !== this.timelineRef.current) { + return this.props.tooltipVisible && this.hideTimeTooltip() + } + const time = this.getTime(e); + const { endTime, liveTimeTravel } = this.props; - const scale = 100 / endTime; + const timeLineTooltip = { + time: liveTimeTravel ? endTime - time : time, + offset: e.nativeEvent.offsetX, + isVisible: true + } + debounceTooltipChange(timeLineTooltip) + } - return ( -
-
- - - - {skip && - skipIntervals.map((interval) => ( -
- ))} -
- {events.map((e) => ( -
- ))} - {/* {issues.map((iss) => ( -
- - {iss.name} -
- } - > - - -
- ))} */} - {/* { events.filter(e => e.type === TYPES.CLICKRAGE).map(e => ( + hideTimeTooltip = () => { + const timeLineTooltip = { isVisible: false } + debounceTooltipChange(timeLineTooltip) + } + + render() { + const { + events, + skip, + skipIntervals, + disabled, + endTime, + exceptionsList, + resourceList, + clickRageTime, + stackList, + fetchList, + issues, + liveTimeTravel, + } = this.props; + + const scale = 100 / endTime; + + return ( +
+
+ + {/* custo color is live */} + + + + + { skip && skipIntervals.map(interval => + (
)) + } +
+ + { events.map(e => ( +
+ )) + } + { + issues.map(iss => ( +
+ + { iss.name } +
+ } + > + + +
+ )) + } + { events.filter(e => e.type === TYPES.CLICKRAGE).map(e => (
- ))} */} - {/* {typeof clickRageTime === 'number' && + ))} + {typeof clickRageTime === 'number' &&
- } */} - {/* { exceptionsList + } + { exceptionsList .map(e => (
)) - } */} - {/* { resourceList + } + { resourceList .filter(r => r.isRed() || r.isYellow()) .map(r => (
)) - } */} - {/* { fetchList + } + { fetchList .filter(e => e.isRed()) .map(e => (
)) - } */} - {/* {stackList - .filter((e) => e.isRed()) - .map((e) => ( -
- - Stack Event -
- {e.name} -
- } - > - - -
- ))} */} + } + { stackList + .filter(e => e.isRed()) + .map(e => ( +
+ + Stack Event +
+ { e.name } +
+ } + > + +
-
- ); - } + )) + } +
+
+ ); + } } diff --git a/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx new file mode 100644 index 000000000..be3ac24b3 --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/components/PlayerControls.tsx @@ -0,0 +1,111 @@ +import React from 'react' +import { Tooltip } from 'react-tippy'; +import { ReduxTime } from '../Time'; +import { Icon } from 'UI'; +import cn from 'classnames'; +// @ts-ignore +import styles from '../controls.module.css' + +interface Props { + live: boolean; + skip: boolean; + speed: number; + disabled: boolean; + playButton: JSX.Element; + backTenSeconds: () => void; + forthTenSeconds: () => void; + toggleSpeed: () => void; + toggleSkip: () => void; + controlIcon: (icon: string, size: number, action: () => void, isBackwards: boolean, additionalClasses: string) => JSX.Element; +} + +function PlayerControls(props: Props) { + const { + live, + skip, + speed, + disabled, + playButton, + backTenSeconds, + forthTenSeconds, + toggleSpeed, + toggleSkip, + controlIcon + } = props; + return ( +
+ {playButton} + {!live && ( +
+ {/* @ts-ignore */} + + / + {/* @ts-ignore */} + +
+ )} + +
+ {/* @ts-ignore */} + + {controlIcon( + "skip-forward-fill", + 18, + backTenSeconds, + true, + 'hover:bg-active-blue-border color-main h-full flex items-center' + )} + +
10s
+ {/* @ts-ignore */} + + {controlIcon( + "skip-forward-fill", + 18, + forthTenSeconds, + false, + 'hover:bg-active-blue-border color-main h-full flex items-center' + )} + +
+ + {!live && +
+ {/* @ts-ignore */} + + + + + +
+ } +
+ ) +} + +export default PlayerControls; diff --git a/frontend/app/components/Session_/Player/Controls/components/TooltipContainer.tsx b/frontend/app/components/Session_/Player/Controls/components/TooltipContainer.tsx new file mode 100644 index 000000000..2c90fcc1d --- /dev/null +++ b/frontend/app/components/Session_/Player/Controls/components/TooltipContainer.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import TimeTooltip from '../TimeTooltip'; +import store from 'App/store'; +import { Provider } from 'react-redux'; + +function TooltipContainer({ liveTimeTravel }: { liveTimeTravel: boolean }) { + + return ( + + + + ) +} + +export default React.memo(TooltipContainer); diff --git a/frontend/app/components/Session_/Player/Controls/controls.module.css b/frontend/app/components/Session_/Player/Controls/controls.module.css index 0b377a594..ba04b3396 100644 --- a/frontend/app/components/Session_/Player/Controls/controls.module.css +++ b/frontend/app/components/Session_/Player/Controls/controls.module.css @@ -18,9 +18,6 @@ height: 65px; padding-left: 30px; padding-right: 0; - &[data-is-live=true] { - padding: 0; - } } .buttonsLeft { diff --git a/frontend/app/components/Session_/Player/Controls/timeline.module.css b/frontend/app/components/Session_/Player/Controls/timeline.module.css index a5676d6b1..48217119d 100644 --- a/frontend/app/components/Session_/Player/Controls/timeline.module.css +++ b/frontend/app/components/Session_/Player/Controls/timeline.module.css @@ -21,14 +21,21 @@ } +.greenTracker { + background-color: #42AE5E!important; + box-shadow: 0 0 0 1px #42AE5E; +} + .progress { height: 10px; padding: 8px 0; cursor: pointer; width: 100%; + max-width: 100%; position: relative; display: flex; align-items: center; + } @@ -163,3 +170,28 @@ } } } + +.timeTooltip { + position: absolute; + padding: 0.25rem; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + background: black; + top: -35px; + color: white; + + &:after { + content:''; + position: absolute; + top: 100%; + left: 0; + right: 0; + margin: 0 auto; + width: 0; + height: 0; + border-top: solid 5px black; + border-left: solid 5px transparent; + border-right: solid 5px transparent; + } +} diff --git a/frontend/app/components/Session_/Player/Overlay.tsx b/frontend/app/components/Session_/Player/Overlay.tsx index 994608108..b067a3dd0 100644 --- a/frontend/app/components/Session_/Player/Overlay.tsx +++ b/frontend/app/components/Session_/Player/Overlay.tsx @@ -44,12 +44,6 @@ function Overlay({ togglePlay, closedLive }: Props) { - - // useEffect(() =>{ - // setTimeout(() => markTargets([{ selector: 'div', count:6}]), 5000) - // setTimeout(() => markTargets(null), 8000) - // },[]) - const showAutoplayTimer = !live && completed && autoplay && nextId const showPlayIconLayer = !live && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer; const showLiveStatusText = live && liveStatusText && !loading; @@ -60,7 +54,7 @@ function Overlay({ { showLiveStatusText && } - { messagesLoading && } + { messagesLoading && } { showPlayIconLayer && } @@ -83,4 +77,4 @@ export default connectPlayer(state => ({ concetionStatus: state.peerConnectionStatus, markedTargets: state.markedTargets, activeTargetIndex: state.activeTargetIndex, -}))(Overlay); \ No newline at end of file +}))(Overlay); diff --git a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx index ecf1cb7f0..a99633bb4 100644 --- a/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx +++ b/frontend/app/components/Session_/Player/Overlay/AutoplayTimer.tsx @@ -1,14 +1,19 @@ import React, { useEffect, useState } from 'react' import cn from 'classnames'; import { connect } from 'react-redux' -import { withRouter } from 'react-router-dom'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; import { Button, Link } from 'UI' import { session as sessionRoute, withSiteId } from 'App/routes' import stl from './AutoplayTimer.module.css'; import clsOv from './overlay.module.css'; -function AutoplayTimer({ nextId, siteId, history }) { - let timer +interface IProps extends RouteComponentProps { + nextId: number; + siteId: string; +} + +function AutoplayTimer({ nextId, siteId, history }: IProps) { + let timer: NodeJS.Timer const [cancelled, setCancelled] = useState(false); const [counter, setCounter] = useState(5); @@ -32,7 +37,7 @@ function AutoplayTimer({ nextId, siteId, history }) { } if (cancelled) - return '' + return null return (
@@ -50,7 +55,6 @@ function AutoplayTimer({ nextId, siteId, history }) { ) } - export default withRouter(connect(state => ({ siteId: state.getIn([ 'site', 'siteId' ]), nextId: parseInt(state.getIn([ 'sessions', 'nextId' ])), diff --git a/frontend/app/components/Session_/PlayerBlockHeader.js b/frontend/app/components/Session_/PlayerBlockHeader.js index f0576e419..f7afb2bdb 100644 --- a/frontend/app/components/Session_/PlayerBlockHeader.js +++ b/frontend/app/components/Session_/PlayerBlockHeader.js @@ -105,7 +105,7 @@ export default class PlayerBlockHeader extends React.PureComponent { const { hideBack } = this.state; - const { sessionId, userId, userNumericHash, live, metadata } = session; + const { sessionId, userId, userNumericHash, live, metadata, isCallActive, agentIds } = session; let _metaList = Object.keys(metadata) .filter((i) => metaList.includes(i)) .map((key) => { @@ -142,7 +142,7 @@ export default class PlayerBlockHeader extends React.PureComponent {
)} - {isAssist && } + {isAssist && }
{!isAssist && ( diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index 7dbde4535..c378386ee 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -12,7 +12,6 @@ function SubHeader(props) { const [isCopied, setCopied] = React.useState(false); const isAssist = window.location.pathname.includes('/assist/'); - if (isAssist) return null; const location = props.currentLocation && props.currentLocation.length > 60 ? `${props.currentLocation.slice(0, 60)}...` : props.currentLocation return ( @@ -39,37 +38,39 @@ function SubHeader(props) {
)} -
-
- {!isAssist && props.jiraConfig && props.jiraConfig.token && } + {!isAssist ? ( +
+
+ {props.jiraConfig && props.jiraConfig.token && } +
+
+ + + Share +
+ } + /> +
+
+ +
+
+ +
+
+
-
- - - Share -
- } - /> -
-
- -
-
- -
-
-
-
+ ) : null}
) } diff --git a/frontend/app/components/hocs/index.js b/frontend/app/components/hocs/index.js index 444ad0180..5f08b86f0 100644 --- a/frontend/app/components/hocs/index.js +++ b/frontend/app/components/hocs/index.js @@ -1,2 +1,3 @@ export { default as withRequest } from './withRequest'; -export { default as withToggle } from './withToggle'; \ No newline at end of file +export { default as withToggle } from './withToggle'; +export { default as withCopy } from './withCopy' \ No newline at end of file diff --git a/frontend/app/components/hocs/withCopy.tsx b/frontend/app/components/hocs/withCopy.tsx new file mode 100644 index 000000000..2b3a9d541 --- /dev/null +++ b/frontend/app/components/hocs/withCopy.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import copy from 'copy-to-clipboard'; +import { Tooltip } from 'react-tippy'; + +const withCopy = (WrappedComponent: React.ComponentType) => { + const ComponentWithCopy = (props: any) => { + const [copied, setCopied] = React.useState(false); + const { value, tooltip } = props; + const copyToClipboard = (text: string) => { + copy(text); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 1000); + }; + return ( +
copyToClipboard(value)} className="w-fit"> + + + +
+ ); + }; + return ComponentWithCopy; +}; + +export default withCopy; diff --git a/frontend/app/components/hocs/withRequest.js b/frontend/app/components/hocs/withRequest.js index 80dfaccf3..992b0ce4e 100644 --- a/frontend/app/components/hocs/withRequest.js +++ b/frontend/app/components/hocs/withRequest.js @@ -2,66 +2,66 @@ import React from 'react'; import APIClient from 'App/api_client'; export default ({ - initialData = null, - endpoint = '', - method = 'GET', - requestName = "request", - loadingName = "loading", - errorName = "requestError", - dataName = "data", - dataWrapper = data => data, - loadOnInitialize = false, - resetBeforeRequest = false, // Probably use handler? -}) => BaseComponent => class extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - data: typeof initialData === 'function' ? initialData(props) : initialData, - loading: loadOnInitialize, - error: false, - }; - if (loadOnInitialize) { - this.request(); - } - } + initialData = null, + endpoint = '', + method = 'GET', + requestName = 'request', + loadingName = 'loading', + errorName = 'requestError', + dataName = 'data', + dataWrapper = (data) => data, + loadOnInitialize = false, + resetBeforeRequest = false, // Probably use handler? + }) => + (BaseComponent) => + class extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + data: typeof initialData === 'function' ? initialData(props) : initialData, + loading: loadOnInitialize, + error: false, + }; + if (loadOnInitialize) { + this.request(); + } + } - request = (params, edpParams) => { - this.setState({ - loading: true, - error: false, - data: resetBeforeRequest - ? (typeof initialData === 'function' ? initialData(this.props) : initialData) - : this.state.data, - }); - const edp = typeof endpoint === 'function' - ? endpoint(this.props, edpParams) - : endpoint; - return new APIClient()[ method.toLowerCase() ](edp, params) - .then(response => response.json()) - .then(({ errors, data }) => { - if (errors) { - return this.setError(); - } - this.setState({ - data: dataWrapper(data, this.state.data), - loading: false, - }); - }) - .catch(this.setError); - } + request = (params, edpParams) => { + this.setState({ + loading: true, + error: false, + data: resetBeforeRequest ? (typeof initialData === 'function' ? initialData(this.props) : initialData) : this.state.data, + }); + const edp = typeof endpoint === 'function' ? endpoint(this.props, edpParams) : endpoint; + return new APIClient() + [method.toLowerCase()](edp, params) + .then((response) => response.json()) + .then(({ errors, data }) => { + if (errors) { + return this.setError(); + } + this.setState({ + data: dataWrapper(data, this.state.data), + loading: false, + }); + }) + .catch(this.setError); + }; - setError = () => this.setState({ - loading: false, - error: true, - }) + setError = () => + this.setState({ + loading: false, + error: true, + }); - render() { - const ownProps = { - [ requestName ]: this.request, - [ loadingName ]: this.state.loading, - [ dataName ]: this.state.data, - [ errorName ]: this.state.error, - }; - return - } -} \ No newline at end of file + render() { + const ownProps = { + [requestName]: this.request, + [loadingName]: this.state.loading, + [dataName]: this.state.data, + [errorName]: this.state.error, + }; + return ; + } + }; diff --git a/frontend/app/components/shared/AnimatedSVG/AnimatedSVG.tsx b/frontend/app/components/shared/AnimatedSVG/AnimatedSVG.tsx index 45f4d701d..114cba1e9 100644 --- a/frontend/app/components/shared/AnimatedSVG/AnimatedSVG.tsx +++ b/frontend/app/components/shared/AnimatedSVG/AnimatedSVG.tsx @@ -6,6 +6,10 @@ import DashboardSvg from '../../../svg/dashboard-icn.svg'; import LoaderSVG from '../../../svg/openreplay-preloader.svg'; import SignalGreenSvg from '../../../svg/signal-green.svg'; import SignalRedSvg from '../../../svg/signal-red.svg'; +import NoBookmarks from '../../../svg/ca-no-bookmarked-session.svg'; +import NoLiveSessions from '../../../svg/ca-no-live-sessions.svg'; +import NoSessions from '../../../svg/ca-no-sessions.svg'; +import NoSessionsInVault from '../../../svg/ca-no-sessions-in-vault.svg'; export enum ICONS { DASHBOARD_ICON = 'dashboard-icn', @@ -14,7 +18,11 @@ export enum ICONS { NO_RESULTS = 'no-results', LOADER = 'openreplay-preloader', SIGNAL_GREEN = 'signal-green', - SIGNAL_RED = 'signal-red' + SIGNAL_RED = 'signal-red', + NO_BOOKMARKS = 'ca-no-bookmarked-session', + NO_LIVE_SESSIONS = 'ca-no-live-sessions', + NO_SESSIONS = 'ca-no-sessions', + NO_SESSIONS_IN_VAULT = 'ca-no-sessions-in-vault' } interface Props { @@ -26,28 +34,32 @@ function AnimatedSVG(props: Props) { const renderSvg = () => { switch (name) { case ICONS.LOADER: - return + return ; case ICONS.DASHBOARD_ICON: - return + return ; case ICONS.EMPTY_STATE: - return + return ; case ICONS.LOGO_SMALL: - return + return ; case ICONS.NO_RESULTS: - return + return ; case ICONS.SIGNAL_GREEN: - return + return ; case ICONS.SIGNAL_RED: - return + return ; + case ICONS.NO_BOOKMARKS: + return ; + case ICONS.NO_LIVE_SESSIONS: + return ; + case ICONS.NO_SESSIONS: + return ; + case ICONS.NO_SESSIONS_IN_VAULT: + return ; default: return null; } - } - return ( -
- {renderSvg()} -
- ); + }; + return
{renderSvg()}
; } -export default AnimatedSVG; \ No newline at end of file +export default AnimatedSVG; diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index 154e862a7..3d82dae7c 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -10,7 +10,7 @@ import SubFilterItem from '../SubFilterItem'; interface Props { filterIndex: number; filter: any; // event/filter - onUpdate: (filter) => void; + onUpdate: (filter: any) => void; onRemoveFilter: () => void; isFilter?: boolean; saveRequestPayloads?: boolean; @@ -20,26 +20,26 @@ function FilterItem(props: Props) { const canShowValues = !(filter.operator === 'isAny' || filter.operator === 'onAny' || filter.operator === 'isUndefined'); const isSubFilter = filter.type === FilterType.SUB_FILTERS; - const replaceFilter = (filter) => { + const replaceFilter = (filter: any) => { props.onUpdate({ ...filter, value: [''], - filters: filter.filters ? filter.filters.map((i) => ({ ...i, value: [''] })) : [], + filters: filter.filters ? filter.filters.map((i: any) => ({ ...i, value: [''] })) : [], }); }; - const onOperatorChange = (e, { name, value }) => { + const onOperatorChange = (e: any, { name, value }: any) => { props.onUpdate({ ...filter, operator: value.value }); }; - const onSourceOperatorChange = (e, { name, value }) => { + const onSourceOperatorChange = (e: any, { name, value }: any) => { props.onUpdate({ ...filter, sourceOperator: value.value }); }; - const onUpdateSubFilter = (subFilter, subFilterIndex) => { + const onUpdateSubFilter = (subFilter: any, subFilterIndex: any) => { props.onUpdate({ ...filter, - filters: filter.filters.map((i, index) => { + filters: filter.filters.map((i: any, index: any) => { if (index === subFilterIndex) { return subFilter; } @@ -90,8 +90,8 @@ function FilterItem(props: Props) { {isSubFilter && (
{filter.filters - .filter((i) => (i.key !== FilterKey.FETCH_REQUEST_BODY && i.key !== FilterKey.FETCH_RESPONSE_BODY) || saveRequestPayloads) - .map((subFilter, subFilterIndex) => ( + .filter((i: any) => (i.key !== FilterKey.FETCH_REQUEST_BODY && i.key !== FilterKey.FETCH_RESPONSE_BODY) || saveRequestPayloads) + .map((subFilter: any, subFilterIndex: any) => ( void; + filter: any; + onUpdate: (filter: any) => void; } function FilterSource(props: Props) { - const { filter } = props; - const [value, setValue] = useState(filter.source[0] || ''); + const { filter } = props; + const [value, setValue] = useState(filter.source[0] || ''); + const debounceUpdate: any = React.useCallback(debounce(props.onUpdate, 1000), [props.onUpdate]); - const onChange = ({ target: { value, name } }) => { - props.onUpdate({ ...filter, [name]: [value] }) - } + useEffect(() => { + setValue(filter.source[0] || ''); + }, [filter]); - useEffect(() => { - setValue(filter.source[0] || ''); - }, [filter]) + useEffect(() => { + debounceUpdate({ ...filter, source: [value] }); + }, [value]); - useEffect(() => { - props.onUpdate({ ...filter, source: [value] }) - }, [value]) + const write = ({ target: { value, name } }: any) => setValue(value); - const write = ({ target: { value, name } }) => setValue(value) + const renderFiled = () => { + switch (filter.sourceType) { + case FilterType.NUMBER: + return ( +
+ +
{filter.sourceUnit}
+
+ ); + } + }; - const renderFiled = () => { - switch(filter.sourceType) { - case FilterType.NUMBER: - return ( - - ) - } - } - - return ( -
- { renderFiled()} -
- ); + return
{renderFiled()}
; } -export default FilterSource; \ No newline at end of file +export default FilterSource; diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 29dce323d..5638f9a1d 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -6,6 +6,7 @@ import FilterValueDropdown from '../FilterValueDropdown'; import FilterDuration from '../FilterDuration'; import { debounce } from 'App/utils'; import { assist as assistRoute, isRoute } from 'App/routes'; +import cn from 'classnames'; const ASSIST_ROUTE = assistRoute(); @@ -172,7 +173,8 @@ function FilterValue(props: Props) { }; return ( -
+ // +
{filter.type === FilterType.DURATION ? renderValueFiled(filter.value, 0) : filter.value && diff --git a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx index f4bb1f45d..97866a270 100644 --- a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx +++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx @@ -1,6 +1,6 @@ -import React, { useEffect } from 'react'; +import React, { Fragment, useEffect } from 'react'; import { connect } from 'react-redux'; -import { NoContent, Loader, Pagination } from 'UI'; +import { NoContent, Loader, Pagination, Button } from 'UI'; import { List } from 'immutable'; import SessionItem from 'Shared/SessionItem'; import withPermissions from 'HOCs/withPermissions'; @@ -13,6 +13,8 @@ import SortOrderButton from 'Shared/SortOrderButton'; import { capitalize } from 'App/utils'; import LiveSessionReloadButton from 'Shared/LiveSessionReloadButton'; import cn from 'classnames'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; +import { numberWithCommas } from 'App/utils'; const AUTOREFRESH_INTERVAL = 0.5 * 60 * 1000; const PER_PAGE = 10; @@ -39,11 +41,14 @@ function LiveSessionList(props: Props) { var timeoutId: any; const { filters } = filter; const hasUserFilter = filters.map((i: any) => i.key).includes(KEYS.USERID); - const sortOptions = [{ label: 'Newest', value: 'timestamp' }].concat(metaList - .map((i: any) => ({ - label: capitalize(i), - value: i, - })).toJS()); + const sortOptions = [{ label: 'Newest', value: 'timestamp' }].concat( + metaList + .map((i: any) => ({ + label: capitalize(i), + value: i, + })) + .toJS() + ); useEffect(() => { if (metaListLoading) return; @@ -79,46 +84,57 @@ function LiveSessionList(props: Props) { return (
-
-
-

- Live Sessions - {total} -

+
+
+
+

+ Live Sessions + {numberWithCommas(total)} +

- props.applyFilter({ ...filter })} /> -
-
-
- Sort By -
- i.value === filter.sort) || sortOptions[0]} + /> +
+ props.applyFilter({ order: state })} sortOrder={filter.order} /> +
-
-
- See how to setup the{' '} - - {'Assist'} - {' '} - plugin, if you haven’t done that already. - + title={ +
+ +
+
No live sessions found.
+
} - image={} + subtext={ +
+ + Assist allows you to support your users through live screen viewing and audio/video calls.{' '} + + {'Learn More'} + + + + +
+ } + // image={} show={!loading && list.size === 0} >
@@ -136,18 +152,18 @@ function LiveSessionList(props: Props) { ))}
+ +
+ props.updateCurrentPage(page)} + limit={PER_PAGE} + debounceRequest={500} + /> +
- -
- props.updateCurrentPage(page)} - limit={PER_PAGE} - debounceRequest={500} - /> -
); diff --git a/frontend/app/components/shared/LiveTag/LiveTag.module.css b/frontend/app/components/shared/LiveTag/LiveTag.module.css index cecf45bad..2914b0b76 100644 --- a/frontend/app/components/shared/LiveTag/LiveTag.module.css +++ b/frontend/app/components/shared/LiveTag/LiveTag.module.css @@ -8,26 +8,26 @@ cursor: pointer; user-select: none; height: 26px; - width: 56px; + padding: 4px 8px; border-radius: 3px; - background-color: $gray-light; + background-color: $main; display: flex; align-items: center; justify-content: center; - color: $gray-dark; + color: white; text-transform: uppercase; font-size: 10px; + font-weight: 600; letter-spacing: 1px; margin-right: 10px; & svg { - fill: $gray-dark; + fill: white; + opacity: .5; } &[data-is-live=true] { background-color: #42AE5E; - color: white; & svg { - fill: white; animation: fade 1s infinite; } } -} \ No newline at end of file +} diff --git a/frontend/app/components/shared/LiveTag/LiveTag.tsx b/frontend/app/components/shared/LiveTag/LiveTag.tsx index 36275783a..c29ae3d34 100644 --- a/frontend/app/components/shared/LiveTag/LiveTag.tsx +++ b/frontend/app/components/shared/LiveTag/LiveTag.tsx @@ -10,8 +10,8 @@ interface Props { function LiveTag({ isLive, onClick }: Props) { return ( ) } diff --git a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js index 01f8a66b2..79dc8e436 100644 --- a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js +++ b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js @@ -1,40 +1,55 @@ -import React from 'react' -import { Icon, Button } from 'UI' -import { connect } from 'react-redux' -import { onboarding as onboardingRoute } from 'App/routes' +import React from 'react'; +import { Icon, Button } from 'UI'; +import { connect } from 'react-redux'; +import { onboarding as onboardingRoute } from 'App/routes'; import { withRouter } from 'react-router-dom'; import * as routes from '../../../routes'; const withSiteId = routes.withSiteId; -const NoSessionsMessage= (props) => { - const { sites, match: { params: { siteId } } } = props; - const activeSite = sites.find(s => s.id == siteId); - const showNoSessions = !!activeSite && !activeSite.recorded; - return ( - <> - {showNoSessions && ( -
-
-
-
- -
-
- It takes a few minutes for first recordings to appear. All set but they are still not showing up? Check our troubleshooting section. -
- -
-
-
- )} - - ) -} +const NoSessionsMessage = (props) => { + const { + sites, + match: { + params: { siteId }, + }, + } = props; + const activeSite = sites.find((s) => s.id == siteId); + const showNoSessions = !!activeSite && !activeSite.recorded; + return ( + <> + {showNoSessions && ( +
+
+
+
+ +
+
+ It might take a few minutes for first recording to appear. + + Troubleshoot + + . +
+ +
+
+
+ )} + + ); +}; -export default connect(state => ({ - site: state.getIn([ 'site', 'siteId' ]), - sites: state.getIn([ 'site', 'list' ]) -}))(withRouter(NoSessionsMessage)) \ No newline at end of file +export default connect((state) => ({ + site: state.getIn(['site', 'siteId']), + sites: state.getIn(['site', 'list']), +}))(withRouter(NoSessionsMessage)); diff --git a/frontend/app/components/shared/Select/Select.tsx b/frontend/app/components/shared/Select/Select.tsx index b209eddc8..b58f99132 100644 --- a/frontend/app/components/shared/Select/Select.tsx +++ b/frontend/app/components/shared/Select/Select.tsx @@ -104,7 +104,7 @@ export default function({ placeholder='Select', name const opacity = state.isDisabled ? 0.5 : 1; const transition = 'opacity 300ms'; - return { ...provided, opacity, transition }; + return { ...provided, opacity, transition, fontWeight: '500' }; }, input: (provided: any) => ({ ...provided, diff --git a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx index da8a940a5..7eaabc252 100644 --- a/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx +++ b/frontend/app/components/shared/SelectDateRange/SelectDateRange.tsx @@ -39,7 +39,7 @@ function SelectDateRange(props: Props) { }; const isCustomRange = period.rangeName === CUSTOM_RANGE; - const customRange = isCustomRange ? period.rangeFormatted(undefined, timezone) : ''; + const customRange = isCustomRange ? period.rangeFormatted() : ''; return (
; +} + +export default connect( + (state: any) => ({ + filter: state.getIn(['search', 'instance']), + }), + { sort, applyFilter } +)(SessionSort); diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSort/index.ts b/frontend/app/components/shared/SessionListContainer/components/SessionSort/index.ts new file mode 100644 index 000000000..b0c0489be --- /dev/null +++ b/frontend/app/components/shared/SessionListContainer/components/SessionSort/index.ts @@ -0,0 +1 @@ +export { default } from './SessionSort'; \ No newline at end of file diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSort/sortDropdown.module.css b/frontend/app/components/shared/SessionListContainer/components/SessionSort/sortDropdown.module.css new file mode 100644 index 000000000..87e26bc68 --- /dev/null +++ b/frontend/app/components/shared/SessionListContainer/components/SessionSort/sortDropdown.module.css @@ -0,0 +1,23 @@ +.dropdown { + display: flex !important; + padding: 4px 6px; + border-radius: 3px; + color: $gray-darkest; + font-weight: 500; + &:hover { + background-color: $gray-light; + } +} + +.dropdownTrigger { + padding: 4px 8px; + border-radius: 3px; + &:hover { + background-color: $gray-light; + } +} + +.dropdownIcon { + margin-top: 2px; + margin-left: 3px; +} \ No newline at end of file diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionTags/SessionTags.tsx b/frontend/app/components/shared/SessionListContainer/components/SessionTags/SessionTags.tsx new file mode 100644 index 000000000..22824e6e5 --- /dev/null +++ b/frontend/app/components/shared/SessionListContainer/components/SessionTags/SessionTags.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { setActiveTab } from 'Duck/search'; +import { connect } from 'react-redux'; +import { issues_types } from 'Types/session/issue'; +import { Icon } from 'UI'; +import cn from 'classnames'; + +interface Props { + setActiveTab: typeof setActiveTab; + activeTab: any; + tags: any; + total: number; +} +function SessionTags(props: Props) { + const { activeTab, tags, total } = props; + const disable = activeTab.type === 'all' && total === 0; + + return ( +
+ {tags && + tags.map((tag: any, index: any) => ( +
+ props.setActiveTab(tag)} + label={tag.name} + isActive={activeTab.type === tag.type} + icon={tag.icon} + disabled={disable && tag.type !== 'all'} + /> +
+ ))} +
+ ); +} + +export default connect( + (state: any) => { + const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee'; + return { + activeTab: state.getIn(['search', 'activeTab']), + tags: issues_types.filter((tag: any) => (isEnterprise ? tag.type !== 'bookmark' : tag.type !== 'vault')), + total: state.getIn(['sessions', 'total']) || 0, + }; + }, + { + setActiveTab, + } +)(SessionTags); + +function TagItem({ isActive, onClick, label, icon = '', disabled = false }: any) { + return ( +
+ +
+ ); +} diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionTags/index.ts b/frontend/app/components/shared/SessionListContainer/components/SessionTags/index.ts new file mode 100644 index 000000000..4f5e62f6c --- /dev/null +++ b/frontend/app/components/shared/SessionListContainer/components/SessionTags/index.ts @@ -0,0 +1 @@ +export { default } from './SessionTags'; \ No newline at end of file diff --git a/frontend/app/components/shared/SessionListContainer/index.ts b/frontend/app/components/shared/SessionListContainer/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx index 9796c441c..4f3c3d121 100644 --- a/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx +++ b/frontend/app/components/shared/SessionSearchField/SessionSearchField.tsx @@ -3,64 +3,67 @@ import { connect } from 'react-redux'; import { Input } from 'UI'; import FilterModal from 'Shared/Filters/FilterModal'; import { debounce } from 'App/utils'; -import { assist as assistRoute, isRoute } from "App/routes"; +import { assist as assistRoute, isRoute } from 'App/routes'; const ASSIST_ROUTE = assistRoute(); interface Props { - fetchFilterSearch: (query: any) => void; - addFilterByKeyAndValue: (key: string, value: string) => void; - filterList: any; - filterListLive: any; - filterSearchListLive: any; - filterSearchList: any; + fetchFilterSearch: (query: any) => void; + addFilterByKeyAndValue: (key: string, value: string) => void; + filterList: any; + filterListLive: any; + filterSearchListLive: any; + filterSearchList: any; } function SessionSearchField(props: Props) { - const debounceFetchFilterSearch = React.useCallback(debounce(props.fetchFilterSearch, 1000), []); - const [showModal, setShowModal] = useState(false) - const [searchQuery, setSearchQuery] = useState('') + const debounceFetchFilterSearch = React.useCallback(debounce(props.fetchFilterSearch, 1000), []); + const [showModal, setShowModal] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); - const onSearchChange = ({ target: { value } }: any) => { - setSearchQuery(value) - debounceFetchFilterSearch({ q: value }); - } + const onSearchChange = ({ target: { value } }: any) => { + setSearchQuery(value); + debounceFetchFilterSearch({ q: value }); + }; - const onAddFilter = (filter: any) => { - props.addFilterByKeyAndValue(filter.key, filter.value) - } + const onAddFilter = (filter: any) => { + props.addFilterByKeyAndValue(filter.key, filter.value); + }; - return ( -
- setShowModal(true) } - onBlur={ () => setTimeout(setShowModal, 200, false) } - onChange={ onSearchChange } - placeholder={ 'Search sessions using any captured event (click, input, page, error...)'} - id="search" - type="search" - autoComplete="off" - className="hover:border-gray-medium" - /> + return ( +
+ setShowModal(true)} + onBlur={() => setTimeout(setShowModal, 200, false)} + onChange={onSearchChange} + placeholder={'Search sessions using any captured event (click, input, page, error...)'} + id="search" + type="search" + autoComplete="off" + className="hover:border-gray-medium text-lg placeholder-lg" + /> - { showModal && ( -
- + {showModal && ( +
+ +
+ )}
- )} -
- ); + ); } -export default connect((state: any) => ({ - filterSearchList: state.getIn([ 'search', 'filterSearchList' ]), - filterSearchListLive: state.getIn([ 'liveSearch', 'filterSearchList' ]), - filterList: state.getIn([ 'search', 'filterList' ]), - filterListLive: state.getIn([ 'search', 'filterListLive' ]), -}), { })(SessionSearchField); +export default connect( + (state: any) => ({ + filterSearchList: state.getIn(['search', 'filterSearchList']), + filterSearchListLive: state.getIn(['liveSearch', 'filterSearchList']), + filterList: state.getIn(['search', 'filterList']), + filterListLive: state.getIn(['search', 'filterListLive']), + }), + {} +)(SessionSearchField); diff --git a/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx b/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx index 5ee40a489..dc14f6245 100644 --- a/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx +++ b/frontend/app/components/shared/SessionSettings/components/DefaultTimezone.tsx @@ -8,36 +8,10 @@ import { toast } from 'react-toastify'; type TimezonesDropdown = Timezone[] -const generateGMTZones = (): TimezonesDropdown => { - const timezones: TimezonesDropdown = [] - - const positiveNumbers = [...Array(12).keys()]; - const negativeNumbers = [...Array(12).keys()].reverse(); - negativeNumbers.pop(); // remove trailing zero since we have one in positive numbers array - - const combinedArray = [...negativeNumbers, ...positiveNumbers]; - - for (let i = 0; i < combinedArray.length; i++) { - let symbol = i < 11 ? '-' : '+'; - let isUTC = i === 11 - let prefix = isUTC ? 'UTC / GMT' : 'GMT'; - let value = String(combinedArray[i]).padStart(2, '0'); - - let tz = `${prefix} ${symbol}${String(combinedArray[i]).padStart(2, '0')}:00` - - let dropdownValue = `UTC${symbol}${value}` - timezones.push({ label: tz, value: isUTC ? 'UTC' : dropdownValue }) - } - - timezones.splice(17, 0, { label: 'GMT +05:30', value: 'UTC+05:30' }) - return timezones -} - -const timezoneOptions: TimezonesDropdown = [...generateGMTZones()] - function DefaultTimezone() { const [changed, setChanged] = React.useState(false); const { settingsStore } = useStore(); + const timezoneOptions: TimezonesDropdown = settingsStore.sessionSettings.defaultTimezones; const [timezone, setTimezone] = React.useState(settingsStore.sessionSettings.timezone); const sessionSettings = useObserver(() => settingsStore.sessionSettings); diff --git a/frontend/app/components/shared/SortOrderButton/SortOrderButton.tsx b/frontend/app/components/shared/SortOrderButton/SortOrderButton.tsx index 7d7901783..e693864fe 100644 --- a/frontend/app/components/shared/SortOrderButton/SortOrderButton.tsx +++ b/frontend/app/components/shared/SortOrderButton/SortOrderButton.tsx @@ -14,7 +14,7 @@ export default React.memo(function SortOrderButton(props: Props) {
onChange('asc')} > @@ -23,7 +23,7 @@ export default React.memo(function SortOrderButton(props: Props) {
onChange('desc')} > diff --git a/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js b/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js index cd8c23707..586cc8742 100644 --- a/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js +++ b/frontend/app/components/shared/TrackingCodeModal/TrackingCodeModal.js @@ -3,65 +3,79 @@ import { Modal, Icon, Tabs } from 'UI'; import styles from './trackingCodeModal.module.css'; import { editGDPR, saveGDPR } from 'Duck/site'; import { connect } from 'react-redux'; -import ProjectCodeSnippet from './ProjectCodeSnippet'; +import ProjectCodeSnippet from './ProjectCodeSnippet'; import InstallDocs from './InstallDocs'; import cn from 'classnames'; const PROJECT = 'Using Script'; const DOCUMENTATION = 'Using NPM'; const TABS = [ - { key: DOCUMENTATION, text: DOCUMENTATION }, - { key: PROJECT, text: PROJECT }, + { key: DOCUMENTATION, text: DOCUMENTATION }, + { key: PROJECT, text: PROJECT }, ]; class TrackingCodeModal extends React.PureComponent { - state = { copied: false, changed: false, activeTab: DOCUMENTATION }; + state = { copied: false, changed: false, activeTab: DOCUMENTATION }; - setActiveTab = (tab) => { - this.setState({ activeTab: tab }); - } + setActiveTab = (tab) => { + this.setState({ activeTab: tab }); + }; - renderActiveTab = () => { - const { site } = this.props; - switch (this.state.activeTab) { - case PROJECT: - return ; - case DOCUMENTATION: - return ; + renderActiveTab = () => { + const { site } = this.props; + switch (this.state.activeTab) { + case PROJECT: + return ; + case DOCUMENTATION: + return ; + } + return null; + }; + + render() { + const { site, displayed, onClose, title = '', subTitle } = this.props; + const { activeTab } = this.state; + return ( +
+

+ {title} {subTitle && {subTitle}} +

+ +
+ +
{this.renderActiveTab()}
+
+
+ // displayed && + // + // + //
{ title } { subTitle && {subTitle}}
+ //
+ // + //
+ //
+ // + // + //
+ // { this.renderActiveTab() } + //
+ //
+ //
+ ); } - return null; - } - - render() { - const { site, displayed, onClose, title = '', subTitle } = this.props; - const { activeTab } = this.state; - return ( - displayed && - - -
{ title } { subTitle && {subTitle}}
-
- -
-
- - -
- { this.renderActiveTab() } -
-
-
- ); - } } -export default connect(state => ({ - site: state.getIn([ 'site', 'instance' ]), - gdpr: state.getIn([ 'site', 'instance', 'gdpr' ]), - saving: state.getIn([ 'site', 'saveGDPR', 'loading' ]), -}), { - editGDPR, saveGDPR -})(TrackingCodeModal); \ No newline at end of file +export default connect( + (state) => ({ + site: state.getIn(['site', 'instance']), + gdpr: state.getIn(['site', 'instance', 'gdpr']), + saving: state.getIn(['site', 'saveGDPR', 'loading']), + }), + { + editGDPR, + saveGDPR, + } +)(TrackingCodeModal); diff --git a/frontend/app/components/ui/Button/Button.tsx b/frontend/app/components/ui/Button/Button.tsx index bc71be3a0..d3e3acd15 100644 --- a/frontend/app/components/ui/Button/Button.tsx +++ b/frontend/app/components/ui/Button/Button.tsx @@ -30,7 +30,7 @@ export default (props: Props) => { let classes = ['relative flex items-center h-10 px-3 rounded tracking-wide whitespace-nowrap']; if (variant === 'default') { - classes.push('bg-white hover:bg-gray-lightest border border-gray-light'); + classes.push('bg-white hover:bg-gray-light border border-gray-light'); } if (variant === 'primary') { @@ -38,7 +38,7 @@ export default (props: Props) => { } if (variant === 'text') { - classes.push('bg-transparent color-gray-dark hover:bg-gray-lightest hover:color-gray-dark'); + classes.push('bg-transparent color-gray-dark hover:bg-gray-light hover:color-gray-dark'); } if (variant === 'text-primary') { diff --git a/frontend/app/components/ui/Form/Form.tsx b/frontend/app/components/ui/Form/Form.tsx index c9ab7c036..a85af0b23 100644 --- a/frontend/app/components/ui/Form/Form.tsx +++ b/frontend/app/components/ui/Form/Form.tsx @@ -2,16 +2,15 @@ import React from 'react'; interface Props { children: React.ReactNode; - onSubmit?: any - [x: string]: any + onSubmit?: any; + [x: string]: any; } - interface FormFieldProps { children: React.ReactNode; - [x: string]: any + [x: string]: any; } -function FormField (props: FormFieldProps) { +function FormField(props: FormFieldProps) { const { children, ...rest } = props; return (
@@ -20,16 +19,18 @@ function FormField (props: FormFieldProps) { ); } - function Form(props: Props) { const { children, ...rest } = props; return ( -
{ - e.preventDefault(); - if (props.onSubmit) { - props.onSubmit(e); - } - }}> + { + e.preventDefault(); + if (props.onSubmit) { + props.onSubmit(e); + } + }} + > {children}
); @@ -37,4 +38,4 @@ function Form(props: Props) { Form.Field = FormField; -export default Form; \ No newline at end of file +export default Form; diff --git a/frontend/app/components/ui/Input/Input.tsx b/frontend/app/components/ui/Input/Input.tsx index 1897ece13..1c36f7a8a 100644 --- a/frontend/app/components/ui/Input/Input.tsx +++ b/frontend/app/components/ui/Input/Input.tsx @@ -11,13 +11,14 @@ interface Props { rows?: number; [x: string]: any; } -function Input(props: Props) { +const Input = React.forwardRef((props: Props, ref: any) => { const { className = '', leadingButton = '', wrapperClassName = '', icon = '', type = 'text', rows = 4, ...rest } = props; return (
{icon && } {type === 'textarea' ? (