merge dev

This commit is contained in:
Shekar Siri 2022-08-12 12:13:52 +02:00
commit 193e83f0d2
402 changed files with 13105 additions and 9079 deletions

3
api/.gitignore vendored
View file

@ -174,4 +174,5 @@ logs*.txt
SUBNETS.json
./chalicelib/.configs
README/*
README/*
.local

View file

@ -1,7 +1,6 @@
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
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

View file

@ -1,7 +1,6 @@
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
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 \

View file

@ -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):

View file

@ -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 =============")

View file

@ -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

View file

@ -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):

View file

@ -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):

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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
version_number=1.4.0
FS_DIR=/mnt/efs

View file

@ -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

View file

@ -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

View file

@ -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)}}

View file

@ -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(...),

View file

@ -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"

View file

@ -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 \

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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
)

View file

@ -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=

View file

@ -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

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}

View file

@ -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),
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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`,

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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}
}

View file

@ -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
// }

View file

@ -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,

View file

@ -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 {

View file

@ -1,7 +1,6 @@
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
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 \

View file

@ -1,7 +1,6 @@
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
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 \

View file

@ -1,7 +1,6 @@
FROM python:3.10-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
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 \

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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(...),

View file

@ -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() {

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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,
)
}

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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)
)

View file

@ -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 *

View file

@ -1,6 +1,5 @@
FROM node:18-alpine
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
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

View file

@ -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",

View file

@ -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;
}

View file

@ -14,10 +14,10 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
# Default step in docker build
FROM nginx:alpine
LABEL maintainer=Rajesh<rajesh@openreplay.com>
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 && \

View file

@ -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'));

View file

@ -25,6 +25,7 @@ const siteIdRequiredPaths = [
'/custom_metrics',
'/dashboards',
'/metrics',
'/unprocessed',
// '/custom_metrics/sessions',
];

View file

@ -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) ] });
});
};

View file

@ -1,17 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>OpenReplay</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="slack-app-id" content="AA5LEB34M">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
</head>
<body>
<div id="modal-root"></div>
<div id="app"><p style="color: #eee;text-align: center;height: 100%;padding: 25%;">Loading...</p></div>
</body>
<head>
<title>OpenReplay</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="slack-app-id" content="AA5LEB34M" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png" />
<!-- <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet" /> -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="modal-root"></div>
<div id="app"><p style="color: #eee; text-align: center; height: 100%; padding: 25%">Loading...</p></div>
</body>
</html>

View file

@ -0,0 +1,5 @@
<svg width="111" height="66" viewBox="0 0 111 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.3319 24.0791C31.3319 25.4209 31.477 26.5088 31.7308 27.3066C32.0209 28.1044 32.3835 28.9747 32.8912 29.9176C33.0726 30.2077 33.1451 30.4978 33.1451 30.7517C33.1451 31.1143 32.9275 31.4769 32.4561 31.8396L30.1715 33.3627C29.8451 33.5802 29.5187 33.689 29.2286 33.689C28.866 33.689 28.5033 33.5077 28.1407 33.1813C27.633 32.6374 27.1978 32.0572 26.8352 31.4769C26.4726 30.8605 26.1099 30.1715 25.711 29.3374C22.8825 32.6737 19.3286 34.3418 15.0495 34.3418C12.0033 34.3418 9.57366 33.4715 7.79674 31.7308C6.01981 29.9901 5.11322 27.6693 5.11322 24.7682C5.11322 21.6857 6.20113 19.1835 8.41322 17.2978C10.6253 15.4121 13.5627 14.4693 17.2978 14.4693C18.5308 14.4693 19.8 14.578 21.1418 14.7594C22.4835 14.9407 23.8616 15.2308 25.3121 15.5572V12.9099C25.3121 10.1539 24.7319 8.23189 23.6077 7.10772C22.4473 5.98354 20.489 5.43958 17.6967 5.43958C16.4275 5.43958 15.122 5.58464 13.7803 5.91101C12.4385 6.23739 11.133 6.63629 9.86377 7.14398C9.28355 7.39783 8.84838 7.54288 8.59454 7.61541C8.34069 7.68794 8.15937 7.7242 8.01432 7.7242C7.50663 7.7242 7.25278 7.36156 7.25278 6.60002V4.8231C7.25278 4.24288 7.32531 3.80772 7.50663 3.55387C7.68795 3.30002 8.01432 3.04618 8.52201 2.79233C9.79124 2.13959 11.3143 1.59563 13.0912 1.16046C14.8682 0.689036 16.7539 0.471453 18.7484 0.471453C23.0638 0.471453 26.2187 1.45057 28.2495 3.40882C30.244 5.36706 31.2594 8.34068 31.2594 12.3297V24.0791H31.3319ZM16.6088 29.5912C17.8055 29.5912 19.0385 29.3737 20.344 28.9385C21.6495 28.5033 22.8099 27.7055 23.789 26.6176C24.3693 25.9286 24.8044 25.1671 25.022 24.2967C25.2396 23.4264 25.3846 22.3747 25.3846 21.1418V19.6187C24.333 19.3649 23.2088 19.1473 22.0484 19.0022C20.8879 18.8572 19.7638 18.7846 18.6396 18.7846C16.2099 18.7846 14.433 19.2561 13.2363 20.2352C12.0396 21.2143 11.4594 22.5923 11.4594 24.4055C11.4594 26.1099 11.8945 27.3791 12.8011 28.2495C13.6715 29.1561 14.9407 29.5912 16.6088 29.5912ZM45.7286 33.5077C45.0759 33.5077 44.6407 33.3989 44.3506 33.1451C44.0605 32.9275 43.8066 32.4198 43.589 31.7308L35.0671 3.69893C34.8495 2.97365 34.7407 2.50222 34.7407 2.24838C34.7407 1.66816 35.0308 1.34178 35.611 1.34178H39.1649C39.8539 1.34178 40.3253 1.45057 40.5792 1.70442C40.8693 1.922 41.0868 2.4297 41.3044 3.11871L47.3967 27.1253L53.0539 3.11871C53.2352 2.39343 53.4528 1.922 53.7429 1.70442C54.033 1.48684 54.5407 1.34178 55.1934 1.34178H58.0945C58.7835 1.34178 59.255 1.45057 59.5451 1.70442C59.8352 1.922 60.089 2.4297 60.2341 3.11871L65.9638 27.4154L72.2374 3.11871C72.455 2.39343 72.7088 1.922 72.9627 1.70442C73.2528 1.48684 73.7242 1.34178 74.377 1.34178H77.7495C78.3297 1.34178 78.6561 1.63189 78.6561 2.24838C78.6561 2.42969 78.6198 2.61101 78.5835 2.8286C78.5473 3.04618 78.4748 3.33629 78.3297 3.73519L69.5901 31.7671C69.3726 32.4923 69.1187 32.9638 68.8286 33.1813C68.5385 33.3989 68.0671 33.544 67.4506 33.544H64.3319C63.6429 33.544 63.1715 33.4352 62.8813 33.1813C62.5912 32.9275 62.3374 32.4561 62.1923 31.7308L56.5715 8.34068L50.9868 31.6945C50.8055 32.4198 50.5879 32.8912 50.2978 33.1451C50.0077 33.3989 49.5 33.5077 48.8473 33.5077H45.7286ZM92.3275 34.4868C90.4418 34.4868 88.5561 34.2693 86.7429 33.8341C84.9297 33.3989 83.5154 32.9275 82.5726 32.3835C81.9923 32.0572 81.5934 31.6945 81.4484 31.3682C81.3033 31.0418 81.2308 30.6791 81.2308 30.3528V28.5033C81.2308 27.7418 81.5209 27.3791 82.0649 27.3791C82.2824 27.3791 82.5 27.4154 82.7176 27.4879C82.9352 27.5605 83.2616 27.7055 83.6242 27.8506C84.8572 28.3945 86.1989 28.8297 87.6132 29.1198C89.0638 29.4099 90.478 29.555 91.9286 29.555C94.2132 29.555 95.9901 29.1561 97.2231 28.3583C98.4561 27.5605 99.1088 26.4 99.1088 24.9132C99.1088 23.8978 98.7824 23.0638 98.1297 22.3747C97.477 21.6857 96.244 21.0693 94.4671 20.489L89.2088 18.8572C86.5616 18.0231 84.6033 16.7901 83.4066 15.1583C82.2099 13.5627 81.5934 11.7857 81.5934 9.90002C81.5934 8.37695 81.9198 7.03519 82.5726 5.87475C83.2253 4.71431 84.0956 3.69893 85.1835 2.90112C86.2715 2.06706 87.5044 1.45057 88.955 1.01541C90.4055 0.580244 91.9286 0.398926 93.5242 0.398926C94.322 0.398926 95.1561 0.43519 95.9539 0.543981C96.7879 0.652772 97.5495 0.797827 98.311 0.942882C99.0363 1.1242 99.7253 1.30552 100.378 1.5231C101.031 1.74068 101.538 1.95827 101.901 2.17585C102.409 2.46596 102.771 2.75607 102.989 3.08244C103.207 3.37255 103.315 3.77145 103.315 4.27915V5.98354C103.315 6.74508 103.025 7.14398 102.481 7.14398C102.191 7.14398 101.72 6.99893 101.103 6.70882C99.0363 5.76596 96.7154 5.29453 94.1407 5.29453C92.0737 5.29453 90.4418 5.6209 89.3176 6.30991C88.1934 6.99893 87.6132 8.05057 87.6132 9.53739C87.6132 10.5528 87.9759 11.4231 88.7011 12.1121C89.4264 12.8011 90.7682 13.4901 92.6901 14.1066L97.8396 15.7385C100.451 16.5726 102.336 17.733 103.46 19.2198C104.585 20.7066 105.129 22.411 105.129 24.2967C105.129 25.8561 104.802 27.2704 104.186 28.5033C103.533 29.7363 102.663 30.8242 101.538 31.6945C100.414 32.6011 99.0726 33.2539 97.5132 33.7253C95.8813 34.233 94.177 34.4868 92.3275 34.4868Z" fill="#252F3E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M99.1813 52.1111C87.2505 60.9232 69.9165 65.6012 55.0121 65.6012C34.1242 65.6012 15.3033 57.877 1.0879 45.0397C-0.0362783 44.0243 0.979106 42.6463 2.32086 43.4441C17.6967 52.365 36.6626 57.7683 56.2813 57.7683C69.5176 57.7683 84.0593 55.0122 97.4407 49.3551C99.4352 48.4485 101.14 50.6606 99.1813 52.1111Z" fill="#FF9900"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M104.149 46.454C102.626 44.4957 94.0681 45.5111 90.1879 45.9825C89.0274 46.1276 88.8461 45.1122 89.8978 44.3507C96.7154 39.5639 107.921 40.9419 109.226 42.5375C110.532 44.1693 108.864 55.3748 102.481 60.7419C101.502 61.5759 100.559 61.1408 100.994 60.0529C102.445 56.4628 105.673 48.3759 104.149 46.454Z" fill="#FF9900"/>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -1 +1,4 @@
<svg width="2500" height="1719" viewBox="0 0 256 176" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M57.838 170.017c.151 1.663-.051 3.789-.14 5.436h56.864c.053-1.654.091-3.311.091-4.974 0-39.942-15.768-76.266-44.011-104.51C56.704 52.032 40.885 41.31 23.246 33.898L0 86.328c33.989 15.82 54.211 43.783 57.838 83.689zm69.197-1.644c.108 2.371-.062 4.732-.167 7.08h58.177c.077-2.355.13-4.714.13-7.08 0-28.826-5.66-56.82-16.82-83.207-10.767-25.456-26.169-48.306-45.778-67.915a216.421 216.421 0 0 0-15.686-14.218l-37.68 44.315c37.293 33.313 55.304 65.858 57.824 121.025zM235.263 64.39C226.595 41.785 213.935 19.521 198.727 0l-46.95 34.442c27.495 35.099 44.442 79.71 46.058 127.612.152 4.502-.164 8.969-.457 13.399h58.252c.226-4.448.447-8.916.344-13.399-.805-34.945-8.23-65.12-20.71-97.665z" fill="#3676A1"/></svg>
<svg width="58" height="80" viewBox="0 0 58 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.9431 55.1235C30.7258 55.1235 32.1709 53.6784 32.1709 51.8957C32.1709 50.113 30.7258 48.6678 28.9431 48.6678C27.1604 48.6678 25.7153 50.113 25.7153 51.8957C25.7153 53.6784 27.1604 55.1235 28.9431 55.1235Z" fill="#303F9F"/>
<path d="M28.9431 78.9612C21.7674 78.9532 14.8878 76.0991 9.81374 71.0251C4.7397 65.9511 1.8856 59.0715 1.87762 51.8957V38.4743C1.87762 37.9402 2.08961 37.428 2.46701 37.0502C2.8444 36.6724 3.35635 36.4598 3.89038 36.4592H13.4904L13.4579 5.38672L5.90313 10.036V27.7287C5.90313 28.2626 5.69107 28.7745 5.31361 29.152C4.93615 29.5294 4.42419 29.7415 3.89038 29.7415C3.35656 29.7415 2.84461 29.5294 2.46715 29.152C2.08968 28.7745 1.87762 28.2626 1.87762 27.7287V9.80643C1.87915 9.18865 2.03815 8.58147 2.33962 8.04224C2.64108 7.50301 3.07505 7.04955 3.60052 6.72469L11.9692 1.57455C12.5174 1.23696 13.1458 1.05179 13.7895 1.03816C14.4331 1.02454 15.0688 1.18295 15.6308 1.49703C16.1928 1.81112 16.6608 2.26951 16.9865 2.82488C17.3121 3.38025 17.4837 4.01247 17.4834 4.65629L17.5182 36.4592H28.9431C31.9963 36.4587 34.981 37.3637 37.5198 39.0596C40.0587 40.7555 42.0376 43.1662 43.2063 45.9868C44.375 48.8074 44.681 51.9113 44.0856 54.9058C43.4903 57.9003 42.0203 60.6511 39.8615 62.8102C37.7028 64.9692 34.9523 66.4396 31.9578 67.0355C28.9634 67.6313 25.8595 67.3257 23.0387 66.1574C20.2179 64.9891 17.8069 63.0106 16.1106 60.472C14.4143 57.9334 13.5089 54.9489 13.5089 51.8957L13.495 40.487H5.90313V51.8957C5.90313 56.4526 7.2544 60.9071 9.78607 64.696C12.3177 68.485 15.9161 71.4381 20.1261 73.1819C24.3361 74.9257 28.9687 75.382 33.438 74.493C37.9073 73.604 42.0127 71.4097 45.2349 68.1874C48.4571 64.9652 50.6514 60.8599 51.5404 56.3906C52.4294 51.9213 51.9732 47.2887 50.2293 43.0787C48.4855 38.8687 45.5324 35.2703 41.7435 32.7386C37.9546 30.207 33.5 28.8557 28.9431 28.8557H25.451C24.9171 28.8557 24.4052 28.6436 24.0277 28.2662C23.6503 27.8887 23.4382 27.3768 23.4382 26.843C23.4382 26.3091 23.6503 25.7972 24.0277 25.4197C24.4052 25.0423 24.9171 24.8302 25.451 24.8302H28.9431C36.1214 24.8302 43.0056 27.6817 48.0813 32.7575C53.1571 37.8333 56.0086 44.7175 56.0086 51.8957C56.0086 59.0739 53.1571 65.9581 48.0813 71.0339C43.0056 76.1097 36.1214 78.9612 28.9431 78.9612V78.9612ZM17.5228 40.487V51.8934C17.5224 54.1499 18.1911 56.3559 19.4444 58.2324C20.6978 60.1088 22.4794 61.5715 24.564 62.4353C26.6486 63.2992 28.9426 63.5254 31.1558 63.0855C33.3691 62.6455 35.4021 61.5591 36.9979 59.9637C38.5937 58.3683 39.6805 56.3354 40.1208 54.1223C40.5612 51.9092 40.3354 49.6151 39.472 47.5303C38.6086 45.4455 37.1463 43.6636 35.2701 42.4099C33.3939 41.1562 31.1881 40.487 28.9315 40.487H17.5228Z" fill="#303F9F"/>
</svg>

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,6 @@
<svg width="98" height="79" viewBox="0 0 98 79" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M61.7777 21.5287H64.7407L73.1851 13.0842L73.6 9.49903C68.7624 5.22927 62.9163 2.26322 56.6137 0.881054C50.3112 -0.501108 43.7603 -0.253748 37.58 1.59977C31.3996 3.45328 25.7938 6.85177 21.292 11.4742C16.7903 16.0966 13.5412 21.7903 11.8518 28.0176C12.7926 27.632 13.8347 27.5694 14.8148 27.8398L31.7037 25.0546C31.7037 25.0546 32.5629 23.6324 33.0074 23.7213C36.6261 19.7469 41.6271 17.3061 46.9867 16.8985C52.3462 16.4909 57.6588 18.1473 61.837 21.5287H61.7777Z" fill="#EA4335"/>
<path d="M85.2149 28.0176C83.2739 20.8698 79.2887 14.4441 73.7482 9.52869L61.8964 21.3805C64.3664 23.3988 66.3459 25.9516 67.6855 28.8464C69.0251 31.7412 69.6899 34.9025 69.6297 38.0916V40.1954C71.0149 40.1954 72.3865 40.4682 73.6663 40.9983C74.946 41.5284 76.1089 42.3053 77.0884 43.2848C78.0678 44.2643 78.8448 45.4271 79.3749 46.7069C79.905 47.9867 80.1778 49.3583 80.1778 50.7435C80.1778 52.1287 79.905 53.5003 79.3749 54.7801C78.8448 56.0599 78.0678 57.2227 77.0884 58.2022C76.1089 59.1817 74.946 59.9586 73.6663 60.4887C72.3865 61.0188 71.0149 61.2916 69.6297 61.2916H48.5334L46.4297 63.425V76.0768L48.5334 78.1805H69.6297C75.5208 78.2264 81.2701 76.3749 86.0273 72.8999C90.7846 69.4248 94.2969 64.5109 96.0449 58.8849C97.7928 53.259 97.6835 47.2198 95.7331 41.6608C93.7826 36.1018 90.0947 31.3181 85.2149 28.0176V28.0176Z" fill="#4285F4"/>
<path d="M27.4074 78.0621H48.5037V61.1733H27.4074C25.9044 61.1729 24.419 60.8496 23.0519 60.2251L20.0889 61.1436L11.5852 69.5881L10.8445 72.551C15.6132 76.1519 21.432 78.0881 27.4074 78.0621V78.0621Z" fill="#34A853"/>
<path d="M27.4074 23.2768C21.6913 23.3109 16.1286 25.1295 11.4963 28.4786C6.86394 31.8276 3.39326 36.5399 1.56901 41.9571C-0.255244 47.3744 -0.341967 53.2262 1.32095 58.6951C2.98387 64.1641 6.31338 68.9771 10.8445 72.462L23.0815 60.2249C21.5264 59.5224 20.165 58.453 19.1141 57.1086C18.0632 55.7642 17.3541 54.1849 17.0477 52.5062C16.7413 50.8275 16.8468 49.0996 17.355 47.4706C17.8633 45.8416 18.7592 44.3603 19.9658 43.1537C21.1724 41.947 22.6537 41.0512 24.2827 40.5429C25.9117 40.0347 27.6396 39.9292 29.3183 40.2356C30.997 40.542 32.5763 41.251 33.9207 42.302C35.2651 43.3529 36.3345 44.7143 37.0371 46.2694L49.2741 34.0323C46.7056 30.6746 43.3953 27.9565 39.6019 26.0906C35.8085 24.2248 31.6349 23.2617 27.4074 23.2768V23.2768Z" fill="#FBBC05"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -1 +1,12 @@
<svg id="CMYK_-_square" data-name="CMYK - square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 681.02 551.55"><defs><style>.cls-1{fill:#0097a0;}.cls-2{fill:#5bc6cc;}.cls-3{fill:#231f20;}</style></defs><title>NewRelic-logo-square</title><g id="outlines"><path class="cls-1" d="M692.8,220.54C660.86,73.7,484.77-12.68,299.47,27.61s-309.63,192-277.7,338.83,208,233.22,393.32,192.93S724.72,367.37,692.8,220.54ZM344.87,476.79c-103.41,0-187.2-83.82-187.2-187.22s83.8-187.19,187.2-187.19,187.2,83.81,187.2,187.19S448.25,476.79,344.87,476.79Z" transform="translate(-16.78 -17.71)"/><path class="cls-2" d="M391.53,57.56c-132.32,0-239.61,107.28-239.61,239.6S259.21,536.78,391.53,536.78,631.15,429.49,631.15,297.16,523.85,57.56,391.53,57.56ZM344.87,473.78c-101.75,0-184.19-82.47-184.19-184.21S243.12,105.4,344.87,105.4,529,187.85,529,289.57,446.58,473.78,344.87,473.78Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M278.93,271.2l-20.19-42.33c-4.82-10-9.77-21.36-11.46-26.7l-.39.39c.65,7.55.78,17.06.91,25l.52,43.63H233.61V181.08h16.93l21.88,44a164.17,164.17,0,0,1,9.25,23.18l.39-.39c-.39-4.56-1.3-17.45-1.3-25.66l-.26-41.15h14.2V271.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M321.51,242.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.73a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.27,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H346C346,220.93,341.31,214.55,334.15,214.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M437,271.46H423.61l-8.07-30.34c-2.08-7.81-4.3-18-4.3-18H411s-1,6.51-4.3,18.62l-7.94,29.69H385.32l-18-65.25,14.2-2,7.16,31.91c1.82,8.2,3.39,17.32,3.39,17.32h.39a178.91,178.91,0,0,1,3.78-17.71l8.47-30.47h14.07L426.22,235c2.74,10.68,4.17,18.75,4.17,18.75h.39s1.56-10,3.26-17.71l6.77-30.74h14.85Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M267.62,387.2l-7.81-13.94c-6.25-11.07-10.42-17.32-15.37-22.27a7.64,7.64,0,0,0-5.86-2.73V387.2H223.86V297.08h27.48c20.19,0,29.3,11.72,29.3,25.79,0,12.89-8.33,24.75-22.4,24.75,3.26,1.69,9.25,10.42,13.93,18l13.28,21.62Zm-20.84-78h-8.21v28.52h7.68c7.81,0,12-1,14.72-3.78,2.47-2.47,4-6.25,4-10.94C265,313.88,260.06,309.19,246.78,309.19Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M305.12,358.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.72a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.28,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H329.6C329.6,336.93,324.92,330.55,317.75,330.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M371.28,388.63c-14.46,0-14.46-13-14.46-18.62V313.88a106.72,106.72,0,0,0-1.3-19.27l14.72-3.26c1,4,1.17,9.51,1.17,18.1v55.87c0,8.86.39,10.29,1.43,11.85a4,4,0,0,0,4.69,1l2.34,8.86A22.44,22.44,0,0,1,371.28,388.63Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M396.15,311.53A9.34,9.34,0,0,1,386.9,302a9.44,9.44,0,1,1,9.25,9.51ZM389,387.2V322.34l14.46-2.6V387.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M444.46,388.89c-18,0-28-12.63-28-33.86,0-24,14.33-35.42,29-35.42,7.16,0,12.37,1.69,18.23,7.16l-7.16,9.51c-3.91-3.52-7.29-5.08-11.07-5.08a11.2,11.2,0,0,0-10.42,6.64c-2,4-2.73,10.16-2.73,18.36,0,9,1.43,14.72,4.43,18A11.58,11.58,0,0,0,445.5,378c4.56,0,9-2.21,13.28-6.51l6.77,8.72C459.57,386.16,453.32,388.89,444.46,388.89Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M477.78,388.64A9.67,9.67,0,1,1,487.4,379,9.63,9.63,0,0,1,477.78,388.64Zm0-17.42a7.78,7.78,0,1,0,7.44,7.75A7.55,7.55,0,0,0,477.78,371.22Zm1.9,13.1c-.42-.73-.6-1-1-1.79-1.07-2-1.4-2.5-1.79-2.65a.72.72,0,0,0-.34-.08v4.52H474.4V373.48h4a3,3,0,0,1,3.2,3.17,2.78,2.78,0,0,1-2.42,3,2.47,2.47,0,0,1,.44.47c.62.78,2.6,4.21,2.6,4.21Zm-1.14-8.94a4.35,4.35,0,0,0-1.22-.16h-.78v2.94h.73c.94,0,1.35-.1,1.64-.36a1.53,1.53,0,0,0,.42-1.09A1.28,1.28,0,0,0,478.53,375.38Z" transform="translate(-16.78 -17.71)"/></g></svg>
<svg width="71" height="81" viewBox="0 0 71 81" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_27_2090)">
<path d="M57.0479 28.0403V52.9611L35.3182 65.424V81.0001L70.6399 60.7517V20.2498L57.0479 28.0403Z" fill="#00AC69"/>
<path d="M35.3215 15.5812L57.0512 28.039L70.6433 20.2484L35.3215 0L0 20.2484L13.5868 28.039L35.3215 15.5812Z" fill="#1CE783"/>
<path d="M21.7348 48.2938V73.2146L35.3215 81.0001V40.5032L-6.10352e-05 20.2498V35.8309L21.7348 48.2938Z" fill="black" fill-opacity="0.87"/>
</g>
<defs>
<clipPath id="clip0_27_2090">
<rect width="70.8739" height="81" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 633 B

View file

@ -1,20 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="rollbar-mark-color" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 304 240" style="enable-background:new 0 0 304 240;" xml:space="preserve">
<style type="text/css">
.st0{fill:#3A4757;}
.st1{fill:#F7941D;}
.st2{fill:#BFD730;}
.st3{fill:#00BAD9;}
</style>
<title>rollbar-logo-color-vertical</title>
<g id="icon">
<path class="st0" d="M303.8,239.1V25.7c-0.5-13.6-0.9-34.3-31.4-21.9C221.7,22.4,170.6,40.2,120.3,60C82.2,75,40.5,91.4,19,171.6
c-5.6,21-13.4,46.4-19,67.5h49.4c4.6-17,10.2-38.4,14.8-55.4c15.4-57.4,45.6-69.3,73.2-80.1C176.9,88,217,73.8,257,59.1v179.9
H303.8z"/>
<path class="st1" d="M119,124.5c-5,2.8-9.8,6.1-14.1,9.9c-14.9,13.3-23,32.2-28,51.1l-14.1,51.9H119V124.5z"/>
<path class="st2" d="M180.1,99.7c-12.7,4.7-25.3,9.6-38,14.5c-3.4,1.3-6.7,2.6-10,4v119.2H180L180.1,99.7z"/>
<path class="st3" d="M243.8,237.4v-161c-16.8,6.1-33.7,12.2-50.5,18.4v142.6H243.8z"/>
<svg width="88" height="71" viewBox="0 0 88 71" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_27_2051)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M84.9393 2.09491C84.9382 1.95919 84.9245 1.82386 84.8988 1.69059C84.8988 1.65016 84.8785 1.61377 84.8664 1.57333C84.8543 1.5329 84.8138 1.39139 84.7815 1.30244L84.721 1.17306C84.6834 1.09588 84.64 1.02156 84.5916 0.950679C84.5632 0.906204 84.5351 0.86173 84.5027 0.817255L84.4459 0.740435C84.4136 0.704046 84.3773 0.675744 84.345 0.643397L84.264 0.546361L84.1993 0.501886C84.1401 0.451215 84.0781 0.403969 84.0134 0.360372L83.8556 0.259293C83.7862 0.222856 83.7146 0.190456 83.6415 0.162255L83.4717 0.0935209C83.3949 0.0692618 83.3139 0.0571321 83.2371 0.0409594L83.0633 0.00457045C82.9676 -0.00152348 82.8718 -0.00152348 82.7761 0.00457045H82.6144C81.531 0.101607 67.214 1.46821 51.122 8.64491C41.4586 12.9428 33.9786 19.5414 29.2724 27.5509L28.0596 28.0766C10.8314 35.7666 0.537354 50.7145 0.537354 68.0637V68.3549C0.537763 68.7543 0.652111 69.1453 0.866967 69.4819C1.08183 69.8185 1.38827 70.0867 1.75031 70.2553C2.02817 70.3826 2.33014 70.4488 2.63578 70.4493H59.8268C59.9381 70.4499 60.049 70.4404 60.1584 70.4209L60.3041 70.3847C60.3727 70.3644 60.4416 70.3523 60.5103 70.3281C60.5789 70.3039 60.6112 70.2795 60.6638 70.2553C60.7164 70.2311 60.7851 70.2069 60.8419 70.1746C60.9486 70.1114 61.0499 70.0397 61.1451 69.9602L84.1914 50.5286C84.427 50.3306 84.6158 50.0829 84.7444 49.8031C84.873 49.5235 84.9382 49.219 84.9353 48.9111V2.09491H84.9393ZM64.1005 61.9788L61.889 63.8427V22.501L80.7466 6.60713V47.9489L64.1005 61.9788ZM26.4503 51.0177H57.696V66.2567H8.36905L26.4503 51.0177ZM52.8241 12.4779C60.1892 9.23502 67.8935 6.82515 75.7937 5.29308L58.9375 19.505C51.1569 20.4543 43.5042 22.2563 36.1176 24.8784C40.2903 19.7839 45.9182 15.5588 52.8241 12.4779ZM32.1108 30.9068C40.3192 27.528 48.9123 25.1719 57.696 23.8918V46.825H27.825C28.0404 41.2644 29.5051 35.8237 32.1108 30.9068ZM26.3573 33.5835C24.5907 38.1675 23.6516 43.0286 23.5836 47.9408L4.98487 63.6365C6.39191 50.8844 13.9648 40.2183 26.3573 33.5835Z" fill="#3569F3"/>
</g>
<defs>
<clipPath id="clip0_27_2051">
<rect width="86.7778" height="71" fill="white" transform="translate(0.537354)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -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 }) => (
<div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">{text}</div>
)
const Circle = ({ text }) => <div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">{text}</div>;
const Section = ({ index, title, description, content }) => (
<div className="w-full">
<div className="flex items-start">
<Circle text={index} />
<div>
<span className="font-medium">{title}</span>
{ description && <div className="text-sm color-gray-medium">{description}</div>}
</div>
</div>
<div className="w-full">
<div className="flex items-start">
<Circle text={index} />
<div>
<span className="font-medium">{title}</span>
{description && <div className="text-sm color-gray-medium">{description}</div>}
</div>
</div>
<div className="ml-10">
{content}
<div className="ml-10">{content}</div>
</div>
</div>
)
);
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 (
<Form className={ cn("p-6 pb-10", stl.wrapper)} style={style} onSubmit={() => props.onSubmit(instance)} id="alert-form">
<div className={cn(stl.content, '-mx-6 px-6 pb-12')}>
<input
autoFocus={ true }
className="text-lg border border-gray-light rounded w-full"
name="name"
style={{ fontSize: '18px', padding: '10px', fontWeight: '600'}}
value={ instance && instance.name }
onChange={ write }
placeholder="New Alert"
id="name-field"
/>
<div className="mb-8" />
<Section
index="1"
title={'What kind of alert do you want to set?'}
content={
<div>
<SegmentSelection
primary
name="detectionMethod"
className="my-3"
onSelect={ (e, { name, value }) => props.edit({ [ name ]: value }) }
value={{ value: instance.detectionMethod }}
list={ [
{ name: 'Threshold', value: 'threshold' },
{ name: 'Change', value: 'change' },
]}
/>
<div className="text-sm color-gray-medium">
{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.'}
</div>
<div className="my-4" />
return (
<Form className={cn('p-6 pb-10', stl.wrapper)} style={style} onSubmit={() => props.onSubmit(instance)} id="alert-form">
<div className={cn(stl.content, '-mx-6 px-6 pb-12')}>
<input
autoFocus={true}
className="text-lg border border-gray-light rounded w-full"
name="name"
style={{ fontSize: '18px', padding: '10px', fontWeight: '600' }}
value={instance && instance.name}
onChange={write}
placeholder="Untiltled Alert"
id="name-field"
/>
<div className="mb-8" />
<Section
index="1"
title={'What kind of alert do you want to set?'}
content={
<div>
<SegmentSelection
primary
name="detectionMethod"
className="my-3"
onSelect={(e, { name, value }) => props.edit({ [name]: value })}
value={{ value: instance.detectionMethod }}
list={[
{ name: 'Threshold', value: 'threshold' },
{ name: 'Change', value: 'change' },
]}
/>
<div className="text-sm color-gray-medium">
{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.'}
</div>
<div className="my-4" />
</div>
}
/>
<hr className="my-8" />
<Section
index="2"
title="Condition"
content={
<div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
<Select
className="w-4/6"
placeholder="change"
options={changeOptions}
name="change"
defaultValue={instance.change}
onChange={({ value }) => writeOption(null, { name: 'change', value })}
id="change-dropdown"
/>
</div>
)}
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{isThreshold ? 'Trigger when' : 'of'}</label>
<Select
className="w-4/6"
placeholder="Select Metric"
isSearchable={true}
options={triggerOptions}
name="left"
value={triggerOptions.find((i) => i.value === instance.query.left)}
// onChange={ writeQueryOption }
onChange={({ value }) => writeQueryOption(null, { name: 'left', value: value.value })}
/>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'is'}</label>
<div className="w-4/6 flex items-center">
<Select
placeholder="Select Condition"
options={conditions}
name="operator"
defaultValue={instance.query.operator}
// onChange={ writeQueryOption }
onChange={({ value }) => writeQueryOption(null, { name: 'operator', value: value.value })}
/>
{unit && (
<>
<Input
className="px-4"
style={{ marginRight: '31px' }}
// label={{ basic: true, content: unit }}
// labelPosition='right'
name="right"
value={instance.query.right}
onChange={writeQuery}
placeholder="E.g. 3"
/>
<span className="ml-2">{'test'}</span>
</>
)}
{!unit && (
<Input
wrapperClassName="ml-2"
// className="pl-4"
name="right"
value={instance.query.right}
onChange={writeQuery}
placeholder="Specify Value"
/>
)}
</div>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'over the past'}</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={thresholdOptions}
name="currentPeriod"
defaultValue={instance.currentPeriod}
// onChange={ writeOption }
onChange={({ value }) => writeOption(null, { name: 'currentPeriod', value })}
/>
</div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'compared to previous'}</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={thresholdOptions}
name="previousPeriod"
defaultValue={instance.previousPeriod}
// onChange={ writeOption }
onChange={({ value }) => writeOption(null, { name: 'previousPeriod', value })}
/>
</div>
)}
</div>
}
/>
<hr className="my-8" />
<Section
index="3"
title="Notify Through"
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
content={
<div className="flex flex-col">
<div className="flex items-center my-4">
<Checkbox
name="slack"
className="mr-8"
type="checkbox"
checked={instance.slack}
onClick={onChangeCheck}
label="Slack"
/>
<Checkbox
name="email"
type="checkbox"
checked={instance.email}
onClick={onChangeCheck}
className="mr-8"
label="Email"
/>
<Checkbox name="webhook" type="checkbox" checked={instance.webhook} onClick={onChangeCheck} label="Webhook" />
</div>
{instance.slack && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Slack'}</label>
<div className="w-4/6">
<DropdownChips
fluid
selected={instance.slackInput}
options={slackChannels}
placeholder="Select Channel"
onChange={(selected) => props.edit({ slackInput: selected })}
/>
</div>
</div>
)}
{instance.email && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Email'}</label>
<div className="w-4/6">
<DropdownChips
textFiled
validate={validateEmail}
selected={instance.emailInput}
placeholder="Type and press Enter key"
onChange={(selected) => props.edit({ emailInput: selected })}
/>
</div>
</div>
)}
{instance.webhook && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Webhook'}</label>
<DropdownChips
fluid
selected={instance.webhookInput}
options={webhooks}
placeholder="Select Webhook"
onChange={(selected) => props.edit({ webhookInput: selected })}
/>
</div>
)}
</div>
}
/>
</div>
}
/>
<hr className="my-8" />
<Section
index="2"
title="Condition"
content={
<div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
<Select
className="w-4/6"
placeholder="change"
options={ changeOptions }
name="change"
defaultValue={ instance.change }
onChange={ ({ value }) => writeOption(null , { name: 'change', value }) }
id="change-dropdown"
/>
<div className="flex items-center justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white">
<div className="flex items-center">
<Button loading={loading} variant="primary" type="submit" disabled={loading || !instance.validate()} id="submit-button">
{instance.exists() ? 'Update' : 'Create'}
</Button>
<div className="mx-1" />
<Button onClick={props.onClose}>Cancel</Button>
</div>
)}
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{isThreshold ? 'Trigger when' : 'of'}</label>
<Select
className="w-4/6"
placeholder="Select Metric"
isSearchable={true}
options={ triggerOptions }
name="left"
value={ triggerOptions.find(i => i.value === instance.query.left) }
// onChange={ writeQueryOption }
onChange={ ({ value }) => writeQueryOption(null, { name: 'left', value: value.value }) }
/>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'is'}</label>
<div className="w-4/6 flex items-center">
<Select
placeholder="Select Condition"
options={ conditions }
name="operator"
defaultValue={ instance.query.operator }
// onChange={ writeQueryOption }
onChange={ ({ value }) => writeQueryOption(null, { name: 'operator', value: value.value }) }
/>
{ unit && (
<>
<Input
className="px-4"
style={{ marginRight: '31px'}}
// label={{ basic: true, content: unit }}
// labelPosition='right'
name="right"
value={ instance.query.right }
onChange={ writeQuery }
placeholder="E.g. 3"
/>
<span className="ml-2">{'test'}</span>
</>
)}
{ !unit && (
<Input
wrapperClassName="ml-2"
// className="pl-4"
name="right"
value={ instance.query.right }
onChange={ writeQuery }
placeholder="Specify Value"
/>
)}
<div>
{instance.exists() && (
<Button hover variant="text" loading={deleting} type="button" onClick={() => onDelete(instance)} id="trash-button">
<Icon name="trash" color="gray-medium" size="18" />
</Button>
)}
</div>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'over the past'}</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={ thresholdOptions }
name="currentPeriod"
defaultValue={ instance.currentPeriod }
// onChange={ writeOption }
onChange={ ({ value }) => writeOption(null, { name: 'currentPeriod', value }) }
/>
</div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'compared to previous'}</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={ thresholdOptions }
name="previousPeriod"
defaultValue={ instance.previousPeriod }
// onChange={ writeOption }
onChange={ ({ value }) => writeOption(null, { name: 'previousPeriod', value }) }
/>
</div>
)}
</div>
}
/>
</Form>
);
};
<hr className="my-8" />
<Section
index="3"
title="Notify Through"
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
content={
<div className="flex flex-col">
<div className="flex items-center my-4">
<Checkbox
name="slack"
className="mr-8"
type="checkbox"
checked={ instance.slack }
onClick={ onChangeCheck }
label="Slack"
/>
<Checkbox
name="email"
type="checkbox"
checked={ instance.email }
onClick={ onChangeCheck }
className="mr-8"
label="Email"
/>
<Checkbox
name="webhook"
type="checkbox"
checked={ instance.webhook }
onClick={ onChangeCheck }
label="Webhook"
/>
</div>
{ instance.slack && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Slack'}</label>
<div className="w-4/6">
<DropdownChips
fluid
selected={instance.slackInput}
options={slackChannels}
placeholder="Select Channel"
onChange={(selected) => props.edit({ 'slackInput': selected })}
/>
</div>
</div>
)}
{instance.email && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Email'}</label>
<div className="w-4/6">
<DropdownChips
textFiled
validate={validateEmail}
selected={instance.emailInput}
placeholder="Type and press Enter key"
onChange={(selected) => props.edit({ 'emailInput': selected })}
/>
</div>
</div>
)}
{instance.webhook && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Webhook'}</label>
<DropdownChips
fluid
selected={instance.webhookInput}
options={webhooks}
placeholder="Select Webhook"
onChange={(selected) => props.edit({ 'webhookInput': selected })}
/>
</div>
)}
</div>
}
/>
</div>
<div className="flex items-center justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white">
<div className="flex items-center">
<Button
loading={loading}
variant="primary"
type="submit"
disabled={loading || !instance.validate()}
id="submit-button"
>
{instance.exists() ? 'Update' : 'Create'}
</Button>
<div className="mx-1" />
<Button onClick={props.onClose}>Cancel</Button>
</div>
<div>
{instance.exists() && (
<Button
hover
variant="text"
loading={deleting}
type="button"
onClick={() => onDelete(instance)}
id="trash-button"
>
<Icon name="trash" color="gray-medium" size="18" />
</Button>
)}
</div>
</div>
</Form>
)
}
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);

View file

@ -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 (
<SlideModal
title={
<div className="flex items-center">
<span className="mr-3">{ 'Create Alert' }</span>
{/* <IconButton
circle
size="small"
icon="plus"
outline
id="add-button"
onClick={ () => toggleForm({}, true) }
/> */}
</div>
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 &&
<AlertForm
metricId={ metricId }
edit={props.edit}
slackChannels={slackChannels}
webhooks={hooks}
onSubmit={saveAlert}
};
const toggleForm = (instance, state) => {
if (instance) {
props.init(instance);
}
return setShowForm(state ? state : !showForm);
};
return (
<SlideModal
title={
<div className="flex items-center">
<span className="mr-3">{'Create Alert'}</span>
</div>
}
isDisplayed={showModal}
onClose={props.onClose}
onDelete={onDelete}
style={{ width: '580px', height: '100vh - 200px' }}
/>
}
/>
);
size="medium"
content={
showModal && (
<AlertForm
metricId={metricId}
edit={props.edit}
slackChannels={slackChannels}
webhooks={hooks}
onSubmit={saveAlert}
onClose={props.onClose}
onDelete={onDelete}
style={{ width: '580px', height: '100vh - 200px' }}
/>
)
}
/>
);
}
export default connect(state => ({
webhooks: state.getIn(['webhooks', 'list']),
instance: state.getIn(['alerts', 'instance']),
}), { init, edit, save, remove, fetchWebhooks, setShowAlerts })(AlertFormModal)
export default connect(
(state) => ({
webhooks: state.getIn(['webhooks', 'list']),
instance: state.getIn(['alerts', 'instance']),
}),
{ init, edit, save, remove, fetchWebhooks, setShowAlerts }
)(AlertFormModal);

View file

@ -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 (
<div>
<SlideModal
title={
<div className="flex items-center">
<span className="mr-3">{ 'Alerts' }</span>
<IconButton
circle
size="small"
icon="plus"
outline
id="add-button"
onClick={ () => toggleForm({}, true) }
return (
<div>
<SlideModal
title={
<div className="flex items-center">
<span className="mr-3">{'Alerts'}</span>
<IconButton circle size="small" icon="plus" outline id="add-button" onClick={() => toggleForm({}, true)} />
</div>
}
isDisplayed={true}
onClose={() => {
toggleForm({}, false);
setShowAlerts(false);
}}
size="small"
content={
<AlertsList
onEdit={(alert) => {
toggleForm(alert, true);
}}
onClickCreate={() => toggleForm({}, true)}
/>
}
detailContent={
showForm && (
<AlertForm
edit={props.edit}
slackChannels={slackChannels}
webhooks={hooks}
onSubmit={saveAlert}
onClose={() => toggleForm({}, false)}
onDelete={onDelete}
/>
)
}
/>
</div>
}
isDisplayed={ true }
onClose={ () => {
toggleForm({}, false);
setShowAlerts(false);
} }
size="small"
content={
<AlertsList
onEdit={alert => {
toggleForm(alert, true)
}}
/>
}
detailContent={
showForm && (
<AlertForm
edit={props.edit}
slackChannels={slackChannels}
webhooks={hooks}
onSubmit={saveAlert}
onClose={ () => toggleForm({}, false) }
onDelete={onDelete}
/>
)
}
/>
</div>
)
}
</div>
);
};
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);

View file

@ -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 (
<div>
<div className="mb-3 w-full px-3">
<Input
name="searchQuery"
placeholder="Search by Name or Metric"
onChange={({ target: { value } }) => setQuery(value)}
/>
</div>
<Loader loading={ loading }>
<NoContent
title="No data available."
size="small"
show={ list.size === 0 }
>
<div className="bg-white">
{_filteredList.map(a => (
<div className="border-b" key={a.key}>
<AlertItem
active={instance.alertId === a.alertId}
alert={a}
onEdit={() => onEdit(a.toData())}
/>
</div>
))}
</div>
</NoContent>
</Loader>
</div>
)
}
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 (
<div>
<div className="mb-3 w-full px-3">
<Input name="searchQuery" placeholder="Search by Name or Metric" onChange={({ target: { value } }) => setQuery(value)} />
</div>
<Loader loading={loading}>
<NoContent
title="No alerts have been setup yet."
subtext={
<div className="flex flex-col items-center">
<div>Alerts helps your team stay up to date with the activity on your app.</div>
<Button variant="primary" className="mt-4" icon="plus" onClick={props.onClickCreate}>
Create
</Button>
</div>
}
size="small"
show={list.size === 0}
>
<div className="bg-white">
{_filteredList.map((a) => (
<div className="border-b" key={a.key}>
<AlertItem active={instance.alertId === a.alertId} alert={a} onEdit={() => onEdit(a.toData())} />
</div>
))}
</div>
</NoContent>
</Loader>
</div>
);
};
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);

View file

@ -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 <TagBadge className={badgeClassName} key={text} text={text} hashed={false} onRemove={() => onRemove(val)} outline={true} />;
};
const renderBadge = item => {
const val = typeof item === 'string' ? item : item.value;
const text = typeof item === 'string' ? item : item.label;
return (
<TagBadge
className={badgeClassName}
key={ text }
text={ text }
hashed={false}
onRemove={ () => onRemove(val) }
outline={ true }
/>
)
}
<div className="w-full">
{textFiled ? (
<Input type="text" onKeyPress={onKeyPress} placeholder={placeholder} />
) : (
<Select
placeholder={placeholder}
isSearchable={true}
options={_options}
name="webhookInput"
value={null}
onChange={onSelect}
{...props}
/>
)}
<div className="flex flex-wrap mt-3">
{textFiled ? selected.map(renderBadge) : options.filter((i) => selected.includes(i.value)).map(renderBadge)}
</div>
</div>
);
};
return (
<div className="w-full">
{textFiled ? (
<Input type="text" onKeyPress={onKeyPress} placeholder={placeholder} />
) : (
<Select
placeholder={placeholder}
isSearchable={true}
options={ _options }
name="webhookInput"
value={null}
onChange={ onSelect }
{...props}
/>
)}
<div className="flex flex-wrap mt-3">
{
textFiled ?
selected.map(renderBadge) :
options.filter(i => selected.includes(i.value)).map(renderBadge)
}
</div>
</div>
)
}
export default DropdownChips
export default DropdownChips;

View file

@ -9,9 +9,10 @@ interface Props {
stream: LocalStream | null,
endCall: () => void,
videoEnabled: boolean,
setVideoEnabled: (boolean) => void
isPrestart?: boolean,
setVideoEnabled: (isEnabled: boolean) => void
}
function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled } : Props) {
function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled, isPrestart } : Props) {
const [audioEnabled, setAudioEnabled] = useState(true)
const toggleAudio = () => {
@ -25,6 +26,13 @@ function ChatControls({ stream, endCall, videoEnabled, setVideoEnabled } : Props
.then(setVideoEnabled)
}
/** muting user if he is auto connected to the call */
React.useEffect(() => {
if (isPrestart) {
audioEnabled && toggleAudio();
}
}, [])
return (
<div className={cn(stl.controls, "flex items-center w-full justify-start bottom-0 px-2")}>
<div className="flex items-center">

View file

@ -1,5 +1,4 @@
//@ts-nocheck
import React, { useState, FC, useEffect } from 'react'
import React, { useState, useEffect } from 'react'
import VideoContainer from '../components/VideoContainer'
import cn from 'classnames'
import Counter from 'App/components/shared/SessionItem/Counter'
@ -8,23 +7,23 @@ import ChatControls from '../ChatControls/ChatControls'
import Draggable from 'react-draggable';
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
export interface Props {
incomeStream: MediaStream | null,
incomeStream: MediaStream[] | null,
localStream: LocalStream | null,
userId: String,
userId: string,
isPrestart?: boolean;
endCall: () => void
}
const ChatWindow: FC<Props> = function ChatWindow({ userId, incomeStream, localStream, endCall }) {
function ChatWindow({ userId, incomeStream, localStream, endCall, isPrestart }: Props) {
const [localVideoEnabled, setLocalVideoEnabled] = useState(false)
const [remoteVideoEnabled, setRemoteVideoEnabled] = useState(false)
useEffect(() => {
if (!incomeStream) { return }
if (!incomeStream || incomeStream.length === 0) { return }
const iid = setInterval(() => {
const settings = incomeStream.getVideoTracks()[0]?.getSettings()
const isDummyVideoTrack = !!settings ? (settings.width === 2 || settings.frameRate === 0) : true
const settings = incomeStream.map(stream => stream.getVideoTracks()[0]?.getSettings()).filter(Boolean)
const isDummyVideoTrack = settings.length > 0 ? (settings.every(s => s.width === 2 || s.frameRate === 0 || s.frameRate === undefined)) : true
const shouldBeEnabled = !isDummyVideoTrack
if (shouldBeEnabled !== localVideoEnabled) {
setRemoteVideoEnabled(shouldBeEnabled)
@ -42,16 +41,20 @@ const ChatWindow: FC<Props> = function ChatWindow({ userId, incomeStream, localS
style={{ width: '280px' }}
>
<div className="handle flex items-center p-2 cursor-move select-none border-b">
<div className={stl.headerTitle}><b>Talking to </b> {userId ? userId : 'Anonymous User'}</div>
<div className={stl.headerTitle}>
<b>Talking to </b> {userId ? userId : 'Anonymous User'}
{incomeStream && incomeStream.length > 2 ? ' (+ other agents in the call)' : ''}
</div>
<Counter startTime={new Date().getTime() } className="text-sm ml-auto" />
</div>
<div className={cn(stl.videoWrapper, {'hidden' : minimize}, 'relative')}>
<VideoContainer stream={ incomeStream } />
{!incomeStream && <div className={stl.noVideo}>Error obtaining incoming streams</div>}
{incomeStream && incomeStream.map(stream => <VideoContainer stream={ stream } />)}
<div className="absolute bottom-0 right-0 z-50">
<VideoContainer stream={ localStream ? localStream.stream : null } muted width={50} />
</div>
</div>
<ChatControls videoEnabled={localVideoEnabled} setVideoEnabled={setLocalVideoEnabled} stream={localStream} endCall={endCall} />
<ChatControls videoEnabled={localVideoEnabled} setVideoEnabled={setLocalVideoEnabled} stream={localStream} endCall={endCall} isPrestart={isPrestart} />
</div>
</Draggable>
)

View file

@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
import { Popup, Icon, Button, IconButton } from 'UI';
import logger from 'App/logger';
import { connect } from 'react-redux';
import cn from 'classnames';
import { toggleChatWindow } from 'Duck/sessions';
import { connectPlayer } from 'Player/store';
import ChatWindow from '../../ChatWindow';
import { callPeer, requestReleaseRemoteControl, toggleAnnotation } from 'Player';
import { callPeer, setCallArgs, requestReleaseRemoteControl, toggleAnnotation } from 'Player';
import { CallingState, ConnectionStatus, RemoteControlStatus } from 'Player/MessageDistributor/managers/AssistManager';
import RequestLocalStream from 'Player/MessageDistributor/managers/LocalStream';
import type { LocalStream } from 'Player/MessageDistributor/managers/LocalStream';
@ -14,15 +15,12 @@ import { toast } from 'react-toastify';
import { confirm } from 'UI';
import stl from './AassistActions.module.css';
function onClose(stream) {
stream.getTracks().forEach((t) => t.stop());
}
function onReject() {
toast.info(`Call was rejected.`);
}
function onError(e) {
console.log(e)
toast.error(typeof e === 'string' ? e : e.message);
}
@ -35,6 +33,8 @@ interface Props {
remoteControlStatus: RemoteControlStatus;
hasPermission: boolean;
isEnterprise: boolean;
isCallActive: boolean;
agentIds: string[];
}
function AssistActions({
@ -46,14 +46,21 @@ function AssistActions({
remoteControlStatus,
hasPermission,
isEnterprise,
isCallActive,
agentIds
}: Props) {
const [incomeStream, setIncomeStream] = useState<MediaStream | null>(null);
const [isPrestart, setPrestart] = useState(false);
const [incomeStream, setIncomeStream] = useState<MediaStream[] | null>([]);
const [localStream, setLocalStream] = useState<LocalStream | null>(null);
const [callObject, setCallObject] = useState<{ end: () => void } | null>(null);
const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting;
const cannotCall = peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission);
const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled;
useEffect(() => {
return callObject?.end();
}, []);
return callObject?.end()
}, [])
useEffect(() => {
if (peerConnectionStatus == ConnectionStatus.Disconnected) {
@ -61,15 +68,35 @@ function AssistActions({
}
}, [peerConnectionStatus]);
function call() {
RequestLocalStream()
.then((lStream) => {
setLocalStream(lStream);
setCallObject(callPeer(lStream, setIncomeStream, lStream.stop.bind(lStream), onReject, onError));
})
.catch(onError);
const addIncomeStream = (stream: MediaStream) => {
setIncomeStream(oldState => [...oldState, stream]);
}
function call(agentIds?: string[]) {
RequestLocalStream().then(lStream => {
setLocalStream(lStream);
setCallArgs(
lStream,
addIncomeStream,
lStream.stop.bind(lStream),
onReject,
onError
)
setCallObject(callPeer());
if (agentIds) {
callPeer(agentIds)
}
}).catch(onError)
}
React.useEffect(() => {
if (!onCall && isCallActive && agentIds) {
logger.log('joinig the party', agentIds)
setPrestart(true);
call(agentIds)
}
}, [agentIds, isCallActive])
const confirmCall = async () => {
if (
await confirm({
@ -82,10 +109,6 @@ function AssistActions({
}
};
const onCall = calling === CallingState.OnCall || calling === CallingState.Reconnecting;
const cannotCall = peerConnectionStatus !== ConnectionStatus.Connected || (isEnterprise && !hasPermission);
const remoteActive = remoteControlStatus === RemoteControlStatus.Enabled;
return (
<div className="flex items-center">
{(onCall || remoteActive) && (
@ -123,7 +146,7 @@ function AssistActions({
</div>
<div className={stl.divider} />
<Popup content={cannotCall ? 'You dont have the permissions to perform this action.' : `Call ${userId ? userId : 'User'}`}>
<Popup content={cannotCall ? `You don't have the permissions to perform this action.` : `Call ${userId ? userId : 'User'}`}>
<div
className={cn('cursor-pointer p-2 flex items-center', { [stl.disabled]: cannotCall })}
onClick={onCall ? callObject?.end : confirmCall}
@ -138,7 +161,7 @@ function AssistActions({
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
{onCall && callObject && (
<ChatWindow endCall={callObject.end} userId={userId} incomeStream={incomeStream} localStream={localStream} />
<ChatWindow endCall={callObject.end} userId={userId} incomeStream={incomeStream} localStream={localStream} isPrestart={isPrestart} />
)}
</div>
</div>

View file

@ -4,6 +4,7 @@ import { fetchLiveList } from 'Duck/sessions';
import { Loader, NoContent, Label } from 'UI';
import SessionItem from 'Shared/SessionItem';
import { useModal } from 'App/components/Modal';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
interface Props {
loading: boolean;
@ -24,14 +25,26 @@ function SessionList(props: Props) {
return (
<div style={{ width: '50vw' }}>
<div className="border-r shadow h-screen" style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}>
<div
className="border-r shadow h-screen overflow-y-auto"
style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', minWidth: '700px' }}
>
<div className="p-4">
<div className="text-2xl">
{props.userId}'s <span className="color-gray-medium">Live Sessions</span>{' '}
</div>
</div>
<Loader loading={props.loading}>
<NoContent show={!props.loading && props.list.size === 0} title="No live sessions.">
<NoContent
show={!props.loading && props.list.size === 0}
title={
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_LIVE_SESSIONS} size={170} />
<div className="mt-2" />
<div className="text-center text-gray-600">No live sessions found.</div>
</div>
}
>
<div className="p-4">
{props.list.map((session: any) => (
<div className="mb-6">

View file

@ -26,7 +26,7 @@ function SessionListHeader({ activeTab, count, applyFilter, filter }) {
}, [label]);
const { startDate, endDate, rangeValue } = filter;
const period = new Record({ start: startDate, end: endDate, rangeName: rangeValue });
const period = new Record({ start: startDate, end: endDate, rangeName: rangeValue, timezoneOffset: getTimeZoneOffset() });
const onDateChange = (e) => {
const dateValues = e.toJSON();
@ -36,10 +36,12 @@ function SessionListHeader({ activeTab, count, applyFilter, filter }) {
};
React.useEffect(() => {
const dateValues = period.toJSON();
dateValues.startDate = moment(dateValues.startDate).startOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
dateValues.endDate = moment(dateValues.endDate).endOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
applyFilter(dateValues);
if (label) {
const dateValues = period.toJSON();
dateValues.startDate = moment(dateValues.startDate).startOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
dateValues.endDate = moment(dateValues.endDate).endOf('day').utcOffset(getTimeZoneOffset(), true).valueOf();
applyFilter(dateValues);
}
}, [label]);
return (

View file

@ -52,7 +52,7 @@ export default class Client extends React.PureComponent {
<div className={ styles.tabMenu }>
<PreferencesMenu activeTab={activeTab} />
</div>
<div className={ styles.tabContent }>
<div className="bg-white w-full rounded-lg mx-4 my-6 p-5 border">
{ activeTab && this.renderActiveTab() }
</div>
</div>

View file

@ -4,59 +4,74 @@ import { edit, save } from 'Duck/customField';
import { Form, Input, Button, Message } from 'UI';
import styles from './customFieldForm.module.css';
@connect(state => ({
field: state.getIn(['customFields', 'instance']),
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
errors: state.getIn([ 'customFields', 'saveRequest', 'errors' ]),
}), {
edit,
save,
})
@connect(
(state) => ({
field: state.getIn(['customFields', 'instance']),
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
}),
{
edit,
save,
}
)
class CustomFieldForm 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 });
render() {
const { field, errors} = this.props;
const exists = field.exists();
return (
<Form className={ styles.wrapper }>
<Form.Field>
<label>{'Field Name'}</label>
<Input
ref={ (ref) => { this.focusElement = ref; } }
name="key"
value={ field.key }
onChange={ this.write }
placeholder="Field Name"
/>
</Form.Field>
render() {
const { field, errors } = this.props;
const exists = field.exists();
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
<Form className={styles.wrapper}>
<Form.Field>
<label>{'Field Name'}</label>
<Input
ref={(ref) => {
this.focusElement = ref;
}}
name="key"
value={field.key}
onChange={this.write}
placeholder="Field Name"
/>
</Form.Field>
{ errors &&
<div className="mb-3">
{ errors.map(error => <Message visible={ errors } size="mini" error key={ error } className={ styles.error }>{ error }</Message>) }
</div>
}
{errors && (
<div className="mb-3">
{errors.map((error) => (
<Message visible={errors} size="mini" error key={error} className={styles.error}>
{error}
</Message>
))}
</div>
)}
<Button
onClick={ () => this.props.onSave(field) }
disabled={ !field.validate() }
loading={ this.props.saving }
variant="primary"
className="float-left mr-2"
>
{ exists ? 'Update' : 'Add' }
</Button>
<Button
data-hidden={ !exists }
onClick={ this.props.onClose }
>
{ 'Cancel' }
</Button>
</Form>
);
}
<div className="flex justify-between">
<div className="flex items-center">
<Button
onClick={() => this.props.onSave(field)}
disabled={!field.validate()}
loading={this.props.saving}
variant="primary"
className="float-left mr-2"
>
{exists ? 'Update' : 'Add'}
</Button>
<Button data-hidden={!exists} onClick={this.props.onClose}>
{'Cancel'}
</Button>
</div>
<Button variant="text" icon="trash" data-hidden={!exists} onClick={this.props.onDelete}></Button>
</div>
</Form>
</div>
);
}
}
export default CustomFieldForm;

View file

@ -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 (
<div>
<SlideModal
title={ `${ (field.exists() ? 'Update' : 'Add') + ' Metadata Field' }` }
size="small"
isDisplayed={ showModal }
content={ showModal && <CustomFieldForm onClose={ this.closeModal } onSave={ this.save } /> }
onClose={ this.closeModal }
/>
<div className={ styles.tabHeader }>
<h3 className={ cn(styles.tabTitle, "text-2xl") }>{ 'Metadata' }</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown
value={ currentSite && currentSite.id }
onChange={ this.onChangeSelect }
/>
</div>
<IconButton circle icon="plus" outline onClick={ () => this.init() } />
<TextLink
icon="book"
className="ml-auto color-gray-medium"
href="https://docs.openreplay.com/installation/metadata"
label="Documentation"
/>
</div>
<Loader loading={ loading }>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-6 text-2xl">No data available.</div>
</div>
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"
>
<div className={ styles.list }>
{ fields.filter(i => i.index).map(field => (
<ListItem
disabled={deletingItem && deletingItem === field.index}
key={ field._key }
field={ field }
onEdit={ this.init }
onDelete={ () => this.removeMetadata(field) }
});
};
const init = (field) => {
props.init(field);
showModal(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => 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 (
<div>
<div className={styles.tabHeader}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
</div>
<Button rounded={true} icon="plus" variant="outline" onClick={() => init()} />
<TextLink
icon="book"
className="ml-auto color-gray-medium"
href="https://docs.openreplay.com/installation/metadata"
label="Documentation"
/>
))}
</div>
</NoContent>
</Loader>
</div>
<Loader loading={loading}>
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-6 text-2xl">No data available.</div>
</div>
}
size="small"
show={fields.size === 0}
>
<div className={styles.list}>
{fields
.filter((i) => i.index)
.map((field) => (
<ListItem
disabled={deletingItem && deletingItem === field.index}
key={field._key}
field={field}
onEdit={init}
// onDelete={ () => removeMetadata(field) }
/>
))}
</div>
</NoContent>
</Loader>
</div>
);
}
}
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));

View file

@ -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 (
<div className={ cn(styles.wrapper, field.index === 0 ? styles.preDefined : '', { [styles.disabled] : disabled} ) } onClick={ () => field.index != 0 && onEdit(field) } >
<span>{ field.key }</span>
<div className={ styles.actions } data-hidden={ field.index === 0}>
<div className={ styles.button } onClick={ (e) => { e.stopPropagation(); onDelete(field) } }>
<Icon name="trash" color="teal" size="16" />
const ListItem = ({ field, onEdit, disabled }) => {
return (
<div
className={cn(
'border-b group last:border-none hover:bg-active-blue flex items-center justify-between p-3 cursor-pointer',
field.index === 0 ? styles.preDefined : '',
{
[styles.disabled]: disabled,
}
)}
onClick={() => field.index != 0 && onEdit(field)}
>
<span>{field.key}</span>
<div className="invisible group-hover:visible" data-hidden={field.index === 0}>
<Button variant="text-primary" icon="pencil" />
</div>
</div>
<div className={ styles.button }>
<Icon name="edit" color="teal" size="18"/>
</div>
</div>
</div>
);
);
};
export default ListItem;

View file

@ -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 <AssistScript projectKey={projectKey} />
case NPM:
return <AssistNpm projectKey={projectKey} />
}
return null;
}
const renderActiveTab = () => {
switch (activeTab) {
case SCRIPT:
return <AssistScript projectKey={projectKey} />;
case NPM:
return <AssistNpm projectKey={projectKey} />;
}
return null;
};
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
<h3 className="p-5 text-2xl">Assist</h3>
<div className="p-5">
<div>
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.
</div>
return (
<div className="p-4">
<div>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.</div>
<div className="font-bold my-2">Installation</div>
<Highlight className="js">{`npm i @openreplay/tracker-assist`}</Highlight>
<div className="mb-4" />
<div className="font-bold my-2">Installation</div>
<Highlight className="js">
{`npm i @openreplay/tracker-assist`}
</Highlight>
<div className="mb-4" />
<div className="font-bold my-2">Usage</div>
<Tabs tabs={TABS} active={activeTab} onClick={(tab) => setActiveTab(tab)} />
<div className="font-bold my-2">Usage</div>
<Tabs
tabs={ TABS }
active={ activeTab } onClick={ (tab) => setActiveTab(tab) }
/>
<div className="py-5">{renderActiveTab()}</div>
<div className="py-5">
{ renderActiveTab() }
</div>
<DocLink className="mt-4" label="Install Assist" url="https://docs.openreplay.com/installation/assist" />
</div>
)
<DocLink className="mt-4" label="Install Assist" url="https://docs.openreplay.com/installation/assist" />
</div>
</div>
);
};
AssistDoc.displayName = "AssistDoc";
AssistDoc.displayName = 'AssistDoc';
export default AssistDoc;

View file

@ -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 (
<div className="p-4">
<div>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.</div>
<div className="font-bold my-2">Installation</div>
<Highlight className="js">
{`npm i @openreplay/tracker-axios`}
</Highlight>
<div className="font-bold my-2">Usage</div>
<p>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.</p>
<div className="py-3" />
const { projectKey } = props;
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
<h3 className="p-5 text-2xl">Axios</h3>
<div className="p-5">
<div>
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.
</div>
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import tracker from '@openreplay/tracker';
<div className="font-bold my-2">Installation</div>
<Highlight className="js">{`npm i @openreplay/tracker-axios`}</Highlight>
<div className="font-bold my-2">Usage</div>
<p>
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.
</p>
<div className="py-3" />
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`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();`}
</Highlight>
}
second={
<Highlight className="js">
{`import OpenReplay from '@openreplay/tracker/cjs';
</Highlight>
}
second={
<Highlight className="js">
{`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() {
}, [])
//...
}`}
</Highlight>
}
/>
</Highlight>
}
/>
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/axios" />
</div>
)
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/axios" />
</div>
</div>
);
};
AxiosDoc.displayName = "AxiosDoc";
AxiosDoc.displayName = 'AxiosDoc';
export default AxiosDoc;

View file

@ -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) => (
<>
<div className="p-5 border-b mb-4">
<div>How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Bugsnag" url="https://docs.openreplay.com/integrations/bugsnag" />
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Bugsnag</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Bugsnag" url="https://docs.openreplay.com/integrations/bugsnag" />
</div>
<IntegrationForm
{...props}
name="bugsnag"
formFields={[
{
key: 'authorizationToken',
label: 'Authorisation Token',
},
{
key: 'bugsnagProjectId',
label: 'Project',
checkIfDisplayed: (config) => tokenRE.test(config.authorizationToken),
component: ProjectListDropdown,
},
]}
/>
</div>
<IntegrationForm
{ ...props }
name="bugsnag"
formFields={[ {
key: "authorizationToken",
label: "Authorisation Token",
}, {
key: "bugsnagProjectId",
label: "Project",
checkIfDisplayed: config => tokenRE.test(config.authorizationToken),
component: ProjectListDropdown,
}
]}
/>
</>
);
BugsnagForm.displayName = "BugsnagForm";
BugsnagForm.displayName = 'BugsnagForm';
export default BugsnagForm;
export default BugsnagForm;

View file

@ -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) => (
<>
<div className="p-5 border-b mb-4">
<div>How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.</div>
<DocLink className="mt-4" label="Integrate CloudWatch" url="https://docs.openreplay.com/integrations/cloudwatch" />
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Cloud Watch</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.</div>
<DocLink className="mt-4" label="Integrate CloudWatch" url="https://docs.openreplay.com/integrations/cloudwatch" />
</div>
<IntegrationForm
{...props}
name="cloudwatch"
formFields={[
{
key: 'awsAccessKeyId',
label: 'AWS Access Key ID',
},
{
key: 'awsSecretAccessKey',
label: 'AWS Secret Access Key',
},
{
key: 'region',
label: 'Region',
component: RegionDropdown,
},
{
key: 'logGroupName',
label: 'Log Group Name',
component: LogGroupDropdown,
checkIfDisplayed: (config) =>
config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH &&
config.region !== '' &&
config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH,
},
]}
/>
</div>
<IntegrationForm
{ ...props }
name="cloudwatch"
formFields={[ {
key: "awsAccessKeyId",
label: "AWS Access Key ID",
}, {
key: "awsSecretAccessKey",
label: "AWS Secret Access Key",
}, {
key: "region",
label: "Region",
component: RegionDropdown,
}, {
key: "logGroupName",
label: "Log Group Name",
component: LogGroupDropdown,
checkIfDisplayed: config =>
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;
export default CloudwatchForm;

View file

@ -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) => (
<>
<div className="p-5 border-b mb-4">
<div>How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Datadog" url="https://docs.openreplay.com/integrations/datadog" />
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Datadog</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Datadog" url="https://docs.openreplay.com/integrations/datadog" />
</div>
<IntegrationForm
{...props}
name="datadog"
formFields={[
{
key: 'apiKey',
label: 'API Key',
autoFocus: true,
},
{
key: 'applicationKey',
label: 'Application Key',
},
]}
/>
</div>
<IntegrationForm
{ ...props }
name="datadog"
formFields={[ {
key: "apiKey",
label: "API Key",
autoFocus: true,
}, {
key: "applicationKey",
label: "Application Key",
}
]}
/>
</>
);
DatadogForm.displayName = "DatadogForm";
DatadogForm.displayName = 'DatadogForm';
export default DatadogForm;

View file

@ -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 (
<>
<div className="p-5 border-b mb-4">
<div>How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Elasticsearch" url="https://docs.openreplay.com/integrations/elastic" />
</div>
<IntegrationForm
{ ...props }
name="elasticsearch"
formFields={[ {
key: "host",
label: "Host",
}, {
key: "apiKeyId",
label: "API Key ID",
}, {
key: "apiKey",
label: "API Key",
}, {
key: "indexes",
label: "Indexes",
}, {
key: "port",
label: "Port",
type: "number",
}
]}
/>
</>
)
}
};
render() {
const props = this.props;
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Elasticsearch</h3>
<div className="p-5 border-b mb-4">
<div>How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.</div>
<DocLink className="mt-4" label="Integrate Elasticsearch" url="https://docs.openreplay.com/integrations/elastic" />
</div>
<IntegrationForm
{...props}
name="elasticsearch"
formFields={[
{
key: 'host',
label: 'Host',
},
{
key: 'apiKeyId',
label: 'API Key ID',
},
{
key: 'apiKey',
label: 'API Key',
},
{
key: 'indexes',
label: 'Indexes',
},
{
key: 'port',
label: 'Port',
type: 'number',
},
]}
/>
</div>
);
}
}

View file

@ -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 (
<div className="p-4">
<div>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.</div>
<div className="font-bold my-2">Installation</div>
<Highlight className="js">
{`npm i @openreplay/tracker-fetch --save`}
</Highlight>
<div className="font-bold my-2">Usage</div>
<p>Use the provided fetch method from the plugin instead of the one built-in.</p>
<div className="py-3" />
const { projectKey } = props;
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
<h3 className="p-5 text-2xl">Fetch</h3>
<div className="p-5">
<div>
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.
</div>
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import tracker from '@openreplay/tracker';
<div className="font-bold my-2">Installation</div>
<Highlight className="js">{`npm i @openreplay/tracker-fetch --save`}</Highlight>
<div className="font-bold my-2">Usage</div>
<p>Use the provided fetch method from the plugin instead of the one built-in.</p>
<div className="py-3" />
<div className="font-bold my-2">Usage</div>
<ToggleContent
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`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(<options>)); // check list of available options below
//...
fetch('https://api.openreplay.com/').then(response => console.log(response.json()));`}
</Highlight>
}
second={
<Highlight className="js">
{`import OpenReplay from '@openreplay/tracker/cjs';
</Highlight>
}
second={
<Highlight className="js">
{`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(<options>)); // check list of avai
//...
fetch('https://api.openreplay.com/').then(response => console.log(response.json()));
}`}
</Highlight>
}
/>
</Highlight>
}
/>
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/fetch" />
</div>
)
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/fetch" />
</div>
</div>
);
};
FetchDoc.displayName = "FetchDoc";
FetchDoc.displayName = 'FetchDoc';
export default FetchDoc;

View file

@ -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) => (
<>
<div className="p-5 border-b mb-4">
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
<div className="mt-8">
<DocLink className="mt-4" label="Integrate Github" url="https://docs.openreplay.com/integrations/github" />
</div>
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">Github</h3>
<div className="p-5 border-b mb-4">
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
<div className="mt-8">
<DocLink className="mt-4" label="Integrate Github" url="https://docs.openreplay.com/integrations/github" />
</div>
</div>
<IntegrationForm
{...props}
ignoreProject
name="github"
customPath="github"
formFields={[
{
key: 'token',
label: 'Token',
},
]}
/>
</div>
<IntegrationForm
{ ...props }
ignoreProject
name="issues"
customPath="github"
formFields={[
{
key: "token",
label: "Token",
}
]}
/>
</>
);
GithubForm.displayName = "GithubForm";
GithubForm.displayName = 'GithubForm';
export default GithubForm;
export default GithubForm;

View file

@ -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 (
<div className="p-4">
<p>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.</p>
<p>GraphQL plugin is compatible with Apollo and Relay implementations.</p>
<div className="font-bold my-2">Installation</div>
<Highlight className="js">
{`npm i @openreplay/tracker-graphql --save`}
</Highlight>
<div className="font-bold my-2">Usage</div>
<p>The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It returns result without changes.</p>
<div className="py-3" />
const { projectKey } = props;
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
<h3 className="p-5 text-2xl">GraphQL</h3>
<div className="p-5">
<p>
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.
</p>
<p>GraphQL plugin is compatible with Apollo and Relay implementations.</p>
<ToggleContent
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`import OpenReplay from '@openreplay/tracker';
<div className="font-bold my-2">Installation</div>
<Highlight className="js">{`npm i @openreplay/tracker-graphql --save`}</Highlight>
<div className="font-bold my-2">Usage</div>
<p>
The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It
returns result without changes.
</p>
<div className="py-3" />
<ToggleContent
label="Server-Side-Rendered (SSR)?"
first={
<Highlight className="js">
{`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());`}
</Highlight>
}
second={
<Highlight className="js">
{`import OpenReplay from '@openreplay/tracker/cjs';
</Highlight>
}
second={
<Highlight className="js">
{`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());`}
</Highlight>
}
/>
</Highlight>
}
/>
<DocLink className="mt-4" label="Integrate GraphQL" url="https://docs.openreplay.com/plugins/graphql" />
</div>
)
<DocLink className="mt-4" label="Integrate GraphQL" url="https://docs.openreplay.com/plugins/graphql" />
</div>
</div>
);
};
GraphQLDoc.displayName = "GraphQLDoc";
GraphQLDoc.displayName = 'GraphQLDoc';
export default GraphQLDoc;

View file

@ -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 (
<div className="ph-20">
<Form>
{!ignoreProject &&
<Form.Field>
<label>{ 'OpenReplay Project' }</label>
<SiteDropdown
value={ currentSiteId }
onChange={ this.onChangeSelect }
/>
</Form.Field>
}
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') ?
<Form.Field key={ key }>
<Checkbox
label={label}
name={ key }
value={ config[ key ] }
onChange={ this.write }
placeholder={ placeholder }
type={ Component === 'input' ? type : null }
/>
</Form.Field>
:
<Form.Field key={ key }>
<label>{ label }</label>
<Input
name={ key }
value={ config[ key ] }
onChange={ this.write }
placeholder={ placeholder }
type={ Component === 'input' ? type : null }
autoFocus={autoFocus}
/>
</Form.Field>
)
)}
<Button
onClick={ this.save }
disabled={ !config.validate() }
loading={ saving || loading }
variant="primary"
className="float-left mr-2"
>
{ config.exists() ? 'Update' : 'Add' }
</Button>
return (
<Loader loading={loading}>
<div className="ph-20">
<Form>
{/* {!ignoreProject && (
<Form.Field>
<label>{'OpenReplay Project'}</label>
<SiteDropdown value={currentSiteId} onChange={this.onChangeSelect} />
</Form.Field>
)} */}
{config.exists() && (
<Button
loading={ removing }
onClick={ this.remove }
>
{ 'Delete' }
</Button>
)}
</Form>
</div>
);
}
{formFields.map(
({
key,
label,
placeholder = label,
component: Component = 'input',
type = 'text',
checkIfDisplayed,
autoFocus = false,
}) =>
(typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) &&
(type === 'checkbox' ? (
<Form.Field key={key}>
<Checkbox
label={label}
name={key}
value={config[key]}
onChange={this.write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
/>
</Form.Field>
) : (
<Form.Field key={key}>
<label>{label}</label>
<Input
name={key}
value={config[key]}
onChange={this.write}
placeholder={placeholder}
type={Component === 'input' ? type : null}
autoFocus={autoFocus}
/>
</Form.Field>
))
)}
<Button
onClick={this.save}
disabled={!config.validate()}
loading={saving || loading}
variant="primary"
className="float-left mr-2"
>
{config.exists() ? 'Update' : 'Add'}
</Button>
{config.exists() && (
<Button loading={removing} onClick={this.remove}>
{'Delete'}
</Button>
)}
</Form>
</div>
</Loader>
);
}
}

View file

@ -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 (
<div className={ cn(stl.wrapper, 'mb-4', { [stl.integrated] : integrated })} onClick={ e => onClick(e, url) }>
{integrated && (
<div className="m-2 absolute right-0 top-0 h-4 w-4 rounded-full bg-teal flex items-center justify-center">
<Icon name="check" size="14" color="white" />
</div>
)}
<img className="h-12 w-12" src={'/assets/' + icon + '.svg'} alt="integration" />
<h4 className="my-2">{ title }</h4>
</div>
)
};
export default IntegrationItem;

View file

@ -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<HTMLDivElement, MouseEvent>) => void;
integrated?: boolean;
hide?: boolean;
}
const IntegrationItem = (props: Props) => {
const { integration, integrated, hide = false } = props;
return hide ? <></> : (
<div className={cn(stl.wrapper, 'mb-4', { [stl.integrated]: integrated })} onClick={(e) => props.onClick(e)}>
{integrated && (
<div className="m-2 absolute right-0 top-0 h-4 w-4 rounded-full bg-teal flex items-center justify-center">
<Popup content="Integrated" delay={0}>
<Icon name="check" size="14" color="white" />
</Popup>
</div>
)}
<img className="h-12 w-12" src={'/assets/' + integration.icon + '.svg'} alt="integration" />
<div className="text-center mt-2">
<h4 className="">{integration.title}</h4>
{/* <p className="text-sm color-gray-medium m-0 p-0 h-3">{integration.subtitle && integration.subtitle}</p> */}
</div>
</div>
);
};
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);

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