Merge remote-tracking branch 'origin/api-dev-v1.8.0' into api-v1.8.0

# Conflicts:
#	ee/api/env.default
This commit is contained in:
Taha Yassine Kraiem 2022-08-02 18:53:07 +02:00
commit 66a62edff4
52 changed files with 1271 additions and 820 deletions

3
api/.gitignore vendored
View file

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

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
@ -14,7 +15,7 @@ from routers.crons import core_dynamic_crons
from routers.subs import dashboard, insights, metrics, v1_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

@ -1,6 +1,6 @@
import requests
from decouple import config
from os.path import exists
import schemas
from chalicelib.core import projects
@ -158,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

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

@ -2,6 +2,7 @@ from typing import Union
from decouple import config
from fastapi import Depends, Body, BackgroundTasks, HTTPException
from fastapi.responses import FileResponse
from starlette import status
import schemas
@ -885,6 +886,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)):

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

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

@ -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", docs_url=config("docs_url", default=""), redoc_url=config("redoc_url", default=""))
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.middleware('http')

View file

@ -57,4 +57,5 @@ sourcemaps_bucket=sourcemaps
sourcemaps_reader=http://127.0.0.1:9000/sourcemaps
stage=default-ee
version_number=1.0.0
FS_DIR=/mnt/efs
LEGACY_SEARCH=false

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

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

@ -2,12 +2,12 @@ import React from 'react';
import SessionList from './components/SessionList';
import SessionHeader from './components/SessionHeader';
interface Props {}
function SessionListContainer(props: Props) {
function SessionListContainer() {
return (
<div className="widget-wrapper">
<SessionHeader />
<div className="p-4">
<div className="border-b" />
<div className="px-4">
<SessionList />
</div>
</div>

View file

@ -6,14 +6,25 @@ import SelectDateRange from 'Shared/SelectDateRange';
import SessionTags from '../SessionTags';
import { connect } from 'react-redux';
import SessionSort from '../SessionSort';
import cn from 'classnames';
import { setActiveTab } from 'Duck/search';
import SessionSettingButton from '../SessionSettingButton';
interface Props {
listCount: number;
filter: any;
isBookmark: any;
isEnterprise: boolean;
applyFilter: (filter: any) => void;
setActiveTab: (tab: any) => void;
}
function SessionHeader(props: Props) {
const { listCount, filter: { startDate, endDate, rangeValue } } = props;
const {
listCount,
filter: { startDate, endDate, rangeValue },
isBookmark,
isEnterprise,
} = props;
const period = Period({ start: startDate, end: endDate, rangeName: rangeValue });
const onDateChange = (e: any) => {
@ -22,18 +33,35 @@ function SessionHeader(props: Props) {
};
return (
<div className="flex items-center p-4 justify-between">
<div className="flex items-center">
<div className="mr-3 text-lg">
<span className="font-bold">Sessions</span> <span className="color-gray-medium ml-2">{listCount}</span>
<div className="flex items-center px-4 pt-2 justify-between">
<div className="flex items-center justify-between">
<div className="mr-3 text-lg flex items-center">
<div
className={cn('py-3 cursor-pointer mr-4', {
'border-b color-teal border-teal': !isBookmark,
})}
onClick={() => props.setActiveTab({ type: 'all' })}
>
<span className="font-bold">SESSIONS</span> <span className="color-gray-medium ml-2">{listCount}</span>
</div>
<div
className={cn('py-3 cursor-pointer', {
'border-b color-teal border-teal': isBookmark,
})}
onClick={() => props.setActiveTab({ type: 'bookmark' })}
>
<span className="font-bold">{`${isEnterprise ? 'VAULT' : 'BOOKMARKS'}`}</span>
</div>
</div>
<SessionTags />
</div>
<div className="flex items-center">
{!isBookmark && <SessionTags />}
<div className="mx-4" />
<SelectDateRange period={period} onChange={onDateChange} right={true} />
<div className="mx-2" />
<SessionSort />
<SessionSettingButton />
</div>
</div>
);
@ -43,6 +71,8 @@ export default connect(
(state: any) => ({
filter: state.getIn(['search', 'instance']),
listCount: numberWithCommas(state.getIn(['sessions', 'total'])),
isBookmark: state.getIn(['search', 'activeTab', 'type']) === 'bookmark',
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
}),
{ applyFilter }
{ applyFilter, setActiveTab }
)(SessionHeader);

View file

@ -60,7 +60,7 @@ function SessionList(props: Props) {
show={!loading && list.size === 0}
>
{list.map((session: any) => (
<React.Fragment key={session.sessionId}>
<div key={session.sessionId} className="border-b">
<SessionItem
session={session}
hasUserFilter={hasUserFilter}
@ -68,8 +68,7 @@ function SessionList(props: Props) {
metaList={metaList}
lastPlayedSessionId={lastPlayedSessionId}
/>
<div className="border-b" />
</React.Fragment>
</div>
))}
</NoContent>

View file

@ -0,0 +1,20 @@
import { useModal } from 'App/components/Modal';
import React from 'react';
import SessionSettings from 'Shared/SessionSettings';
import { Button } from 'UI';
function SessionSettingButton(props: any) {
const { showModal } = useModal();
const handleClick = () => {
showModal(<SessionSettings />, { right: true });
};
return (
<div className="cursor-pointer ml-4" onClick={handleClick}>
<Button icon="sliders" variant="text" />
</div>
);
}
export default SessionSettingButton;

View file

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

View file

@ -54,12 +54,11 @@ function TagItem({ isActive, onClick, label, icon = '', disabled = false }: any)
onClick={onClick}
className={cn('transition group rounded ml-2 px-2 py-1 flex items-center uppercase text-sm hover:bg-teal hover:text-white', {
'bg-teal text-white': isActive,
'bg-active-blue color-teal': !isActive,
'disabled': disabled,
})}
>
{icon && <Icon name={icon} color="teal" size="14" className={cn('group-hover:fill-white mr-2', { 'fill-white': isActive })} />}
<span className="leading-none font-bold">{label}</span>
{icon && <Icon name={icon} color={isActive ? 'teal' : 'gray-medium'} size="14" className={cn('group-hover:fill-white mr-2', { 'fill-white': isActive })} />}
<span className="leading-none font-medium">{label}</span>
</button>
</div>
);

View file

@ -0,0 +1,328 @@
import logger from 'App/logger';
import type StatedScreen from '../../StatedScreen';
import type { Message, SetNodeScroll, CreateElementNode } from '../../messages';
import ListWalker from '../ListWalker';
import StylesManager, { rewriteNodeStyleSheet } from './StylesManager';
import { VElement, VText, VFragment, VDocument, VNode, VStyleElement } from './VirtualDOM';
import type { StyleElement } from './VirtualDOM';
type HTMLElementWithValue = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
const IGNORED_ATTRS = [ "autocomplete", "name" ];
const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~
// TODO: filter out non-relevant prefixes
// function replaceCSSPrefixes(css: string) {
// return css
// .replace(/\-ms\-/g, "")
// .replace(/\-webkit\-/g, "")
// .replace(/\-moz\-/g, "")
// .replace(/\-webkit\-/g, "")
// }
export default class DOMManager extends ListWalker<Message> {
private vTexts: Map<number, VText> = new Map() // map vs object here?
private vElements: Map<number, VElement> = new Map()
private vRoots: Map<number, VFragment | VDocument> = new Map()
private upperBodyId: number = -1;
private nodeScrollManagers: Map<number, ListWalker<SetNodeScroll>> = new Map()
private stylesManager: StylesManager
constructor(
private readonly screen: StatedScreen,
private readonly isMobile: boolean,
public readonly time: number
) {
super()
this.stylesManager = new StylesManager(screen)
}
append(m: Message): void {
if (m.tp === "set_node_scroll") {
let scrollManager = this.nodeScrollManagers.get(m.id)
if (!scrollManager) {
scrollManager = new ListWalker()
this.nodeScrollManagers.set(m.id, scrollManager)
}
scrollManager.append(m)
return
}
if (m.tp === "create_element_node") {
if(m.tag === "BODY" && this.upperBodyId === -1) {
this.upperBodyId = m.id
}
} else if (m.tp === "set_node_attribute" &&
(IGNORED_ATTRS.includes(m.name) || !ATTR_NAME_REGEXP.test(m.name))) {
logger.log("Ignorring message: ", m)
return; // Ignoring
}
super.append(m)
}
private removeBodyScroll(id: number, vn: VElement): void {
if (this.isMobile && this.upperBodyId === id) { // Need more type safety!
(vn.node as HTMLBodyElement).style.overflow = "hidden"
}
}
// May be make it as a message on message add?
private removeAutocomplete(node: Element): boolean {
const tag = node.tagName
if ([ "FORM", "TEXTAREA", "SELECT" ].includes(tag)) {
node.setAttribute("autocomplete", "off");
return true;
}
if (tag === "INPUT") {
node.setAttribute("autocomplete", "new-password");
return true;
}
return false;
}
private insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void {
const child = this.vElements.get(id) || this.vTexts.get(id)
if (!child) {
logger.error("Insert error. Node not found", id);
return;
}
const parent = this.vElements.get(parentID) || this.vRoots.get(parentID)
if (!parent) {
logger.error("Insert error. Parent node not found", parentID);
return;
}
const pNode = parent.node
if ((pNode instanceof HTMLStyleElement) && // TODO: correct ordering OR filter in tracker
pNode.sheet &&
pNode.sheet.cssRules &&
pNode.sheet.cssRules.length > 0 &&
pNode.innerText.trim().length === 0
) {
logger.log("Trying to insert child to a style tag with virtual rules: ", parent, child);
return;
}
parent.insertChildAt(child, index)
}
private applyMessage = (msg: Message): void => {
let node: Node | undefined
let vn: VNode | undefined
let doc: Document | null
switch (msg.tp) {
case "create_document":
doc = this.screen.document;
if (!doc) {
logger.error("No iframe document found", msg)
return;
}
doc.open();
doc.write("<!DOCTYPE html><html></html>");
doc.close();
const fRoot = doc.documentElement;
fRoot.innerText = '';
vn = new VElement(fRoot)
this.vElements = new Map([[0, vn]])
const vDoc = new VDocument(doc)
vDoc.insertChildAt(vn, 0)
this.vRoots = new Map([[-1, vDoc]]) // todo: start from 0 (sync logic with tracker)
this.stylesManager.reset()
return
case "create_text_node":
vn = new VText()
this.vTexts.set(msg.id, vn)
this.insertNode(msg)
return
case "create_element_node":
let element: Element
if (msg.svg) {
element = document.createElementNS('http://www.w3.org/2000/svg', msg.tag)
} else {
element = document.createElement(msg.tag)
}
if (msg.tag === "STYLE" || msg.tag === "style") {
vn = new VStyleElement(element as StyleElement)
} else {
vn = new VElement(element)
}
this.vElements.set(msg.id, vn)
this.insertNode(msg)
this.removeBodyScroll(msg.id, vn)
this.removeAutocomplete(element)
if (['STYLE', 'style', 'LINK'].includes(msg.tag)) { // Styles in priority
vn.enforceInsertion()
}
return
case "move_node":
this.insertNode(msg);
return
case "remove_node":
vn = this.vElements.get(msg.id) || this.vTexts.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!vn.parentNode) { logger.error("Parent node not found", msg); return }
vn.parentNode.removeChild(vn)
return
case "set_node_attribute":
let { name, value } = msg;
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (name === "href" && vn.node.tagName === "LINK") {
// @ts-ignore ?global ENV type // It've been done on backend (remove after testing in saas)
// if (value.startsWith(window.env.ASSETS_HOST || window.location.origin + '/assets')) {
// value = value.replace("?", "%3F");
// }
if (!value.startsWith("http")) { return }
// blob:... value happened here. https://foss.openreplay.com/3/session/7013553567419137
// that resulted in that link being unable to load and having 4sec timeout in the below function.
this.stylesManager.setStyleHandlers(vn.node as HTMLLinkElement, value);
}
if (vn.node.namespaceURI === 'http://www.w3.org/2000/svg' && value.startsWith("url(")) {
value = "url(#" + (value.split("#")[1] ||")")
}
vn.setAttribute(name, value)
this.removeBodyScroll(msg.id, vn)
return
case "remove_node_attribute":
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
vn.removeAttribute(msg.name)
return
case "set_input_value":
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
const nodeWithValue = vn.node
if (!(nodeWithValue instanceof HTMLInputElement
|| nodeWithValue instanceof HTMLTextAreaElement
|| nodeWithValue instanceof HTMLSelectElement)
) {
logger.error("Trying to set value of non-Input element", msg)
return
}
const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value
doc = this.screen.document
if (doc && nodeWithValue === doc.activeElement) {
// For the case of Remote Control
nodeWithValue.onblur = () => { nodeWithValue.value = val }
return
}
nodeWithValue.value = val
return
case "set_input_checked":
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
(vn.node as HTMLInputElement).checked = msg.checked
return
case "set_node_data":
case "set_css_data": // mbtodo: remove css transitions when timeflow is not natural (on jumps)
vn = this.vTexts.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
vn.setData(msg.data)
if (vn.node instanceof HTMLStyleElement) {
doc = this.screen.document
// TODO: move to message parsing
doc && rewriteNodeStyleSheet(doc, vn.node)
}
if (msg.tp === "set_css_data") { // Styles in priority (do we need inlines as well?)
vn.applyChanges()
}
return
case "css_insert_rule":
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!(vn instanceof VStyleElement)) {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg, vn);
return
}
vn.onStyleSheet(sheet => {
try {
sheet.insertRule(msg.rule, msg.index)
} catch (e) {
logger.warn(e, msg)
try {
sheet.insertRule(msg.rule)
} catch (e) {
logger.warn("Cannot insert rule.", e, msg)
}
}
})
return
case "css_delete_rule":
vn = this.vElements.get(msg.id)
if (!vn) { logger.error("Node not found", msg); return }
if (!(vn instanceof VStyleElement)) {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg, vn);
return
}
vn.onStyleSheet(sheet => {
try {
sheet.deleteRule(msg.index)
} catch (e) {
logger.warn(e, msg)
}
})
return
case "create_i_frame_document":
vn = this.vElements.get(msg.frameID)
if (!vn) { logger.error("Node not found", msg); return }
const host = vn.node
if (host instanceof HTMLIFrameElement) {
const vDoc = new VDocument()
this.vRoots.set(msg.id, vDoc)
host.onload = () => {
const doc = host.contentDocument
if (!doc) {
logger.warn("No iframe doc onload", msg, host)
return
}
vDoc.setDocument(doc)
vDoc.applyChanges()
}
return;
} else if (host instanceof Element) { // shadow DOM
try {
const shadowRoot = host.attachShadow({ mode: 'open' })
vn = new VFragment(shadowRoot)
this.vRoots.set(msg.id, vn)
} catch(e) {
logger.warn("Can not attach shadow dom", e, msg)
}
} else {
logger.warn("Context message host is not Element", msg)
}
return
}
}
moveReady(t: number): Promise<void> {
// MBTODO (back jump optimisation):
// - store intemediate virtual dom state
// - cancel previous moveReady tasks (is it possible?) if new timestamp is less
this.moveApply(t, this.applyMessage) // This function autoresets pointer if necessary (better name?)
this.vRoots.forEach(rt => rt.applyChanges()) // MBTODO (optimisation): affected set
// Thinkabout (read): css preload
// What if we go back before it is ready? We'll have two handlres?
return this.stylesManager.moveReady(t).then(() => {
// Apply all scrolls after the styles got applied
this.nodeScrollManagers.forEach(manager => {
const msg = manager.moveGetLast(t)
if (msg) {
const vElm = this.vElements.get(msg.id)
if (vElm) {
vElm.node.scrollLeft = msg.x
vElm.node.scrollTop = msg.y
}
}
})
})
}
}

View file

@ -1,10 +1,10 @@
import type StatedScreen from '../StatedScreen';
import type { CssInsertRule, CssDeleteRule } from '../messages';
import type StatedScreen from '../../StatedScreen';
import type { CssInsertRule, CssDeleteRule } from '../../messages';
type CSSRuleMessage = CssInsertRule | CssDeleteRule;
import logger from 'App/logger';
import ListWalker from './ListWalker';
import ListWalker from '../ListWalker';
const HOVER_CN = "-openreplay-hover";
@ -40,21 +40,21 @@ export default class StylesManager extends ListWalker<CSSRuleMessage> {
}
setStyleHandlers(node: HTMLLinkElement, value: string): void {
let timeoutId;
const promise = new Promise((resolve) => {
if (this.skipCSSLinks.includes(value)) resolve(null);
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const promise = new Promise<void>((resolve) => {
if (this.skipCSSLinks.includes(value)) resolve();
this.linkLoadingCount++;
this.screen.setCSSLoading(true);
const addSkipAndResolve = () => {
this.skipCSSLinks.push(value); // watch out
resolve(null);
resolve()
}
timeoutId = setTimeout(addSkipAndResolve, 4000);
node.onload = () => {
const doc = this.screen.document;
doc && rewriteNodeStyleSheet(doc, node);
resolve(null);
resolve();
}
node.onerror = addSkipAndResolve;
}).then(() => {

View file

@ -0,0 +1,169 @@
type VChild = VElement | VText
export type VNode = VDocument | VFragment | VElement | VText
abstract class VParent {
abstract node: Node | null
protected children: VChild[] = []
private insertedChildren: Set<VChild> = new Set()
insertChildAt(child: VChild, index: number) {
if (child.parentNode) {
child.parentNode.removeChild(child)
}
this.children.splice(index, 0, child)
this.insertedChildren.add(child)
child.parentNode = this
}
removeChild(child: VChild) {
this.children = this.children.filter(ch => ch !== child)
this.insertedChildren.delete(child)
child.parentNode = null
}
applyChanges() {
const node = this.node
if (!node) {
// log err
console.error("No node found", this)
return
}
// inserting
for (let i = this.children.length-1; i >= 0; i--) {
const child = this.children[i]
child.applyChanges()
if (this.insertedChildren.has(child)) {
const nextVSibling = this.children[i+1]
node.insertBefore(child.node, nextVSibling ? nextVSibling.node : null)
}
}
this.insertedChildren.clear()
// removing
const realChildren = node.childNodes
for(let j = 0; j < this.children.length; j++) {
while (realChildren[j] !== this.children[j].node) {
node.removeChild(realChildren[j])
}
}
// removing rest
while(realChildren.length > this.children.length) {
node.removeChild(node.lastChild)
}
}
}
export class VDocument extends VParent {
constructor(public node: Document | null = null) { super() }
setDocument(doc: Document) {
this.node = doc
}
applyChanges() {
if (this.children.length > 1) {
// log err
}
if (!this.node) {
// iframe not mounted yet
return
}
const child = this.children[0]
child.applyChanges()
const htmlNode = child.node
if (htmlNode.parentNode !== this.node) {
this.node.replaceChild(htmlNode, this.node.documentElement)
}
}
}
export class VFragment extends VParent {
constructor(public readonly node: DocumentFragment) { super() }
}
export class VElement extends VParent {
parentNode: VParent | null = null
private newAttributes: Map<string, string | false> = new Map()
constructor(public readonly node: Element) { super() }
setAttribute(name: string, value: string) {
this.newAttributes.set(name, value)
}
removeAttribute(name: string) {
this.newAttributes.set(name, false)
}
// mbtodo: priority insertion instead.
// rn this is for styles that should be inserted as prior,
// otherwise it will show visual styling lag if there is a transition CSS property)
enforceInsertion() {
let vNode: VElement = this
while (vNode.parentNode instanceof VElement) {
vNode = vNode.parentNode
}
(vNode.parentNode || vNode).applyChanges()
}
applyChanges() {
this.newAttributes.forEach((value, key) => {
if (value === false) {
this.node.removeAttribute(key)
} else {
try {
this.node.setAttribute(key, value)
} catch {
// log err
}
}
})
this.newAttributes.clear()
super.applyChanges()
}
}
type StyleSheetCallback = (s: CSSStyleSheet) => void
export type StyleElement = HTMLStyleElement | SVGStyleElement
export class VStyleElement extends VElement {
// private loaded = false
private stylesheetCallbacks: StyleSheetCallback[] = []
constructor(public readonly node: StyleElement) {
super(node) // Is it compiled correctly or with 2 node assignments?
// node.onload = () => {
// const sheet = node.sheet
// if (sheet) {
// this.stylesheetCallbacks.forEach(cb => cb(sheet))
// } else {
// console.warn("Style onload: sheet is null")
// }
// this.loaded = true
// }
}
onStyleSheet(cb: StyleSheetCallback) {
// if (this.loaded) {
if (!this.node.sheet) {
console.warn("Style tag is loaded, but sheet is null")
return
}
cb(this.node.sheet)
// } else {
// this.stylesheetCallbacks.push(cb)
// }
}
}
export class VText {
parentNode: VParent | null = null
constructor(public readonly node: Text = new Text()) {}
private data: string = ""
private changed: boolean = false
setData(data: string) {
this.data = data
this.changed = true
}
applyChanges() {
if (this.changed) {
this.node.data = this.data
this.changed = false
}
}
}

View file

@ -1,322 +0,0 @@
import type StatedScreen from '../StatedScreen';
import type { Message, SetNodeScroll, CreateElementNode } from '../messages';
import logger from 'App/logger';
import StylesManager, { rewriteNodeStyleSheet } from './StylesManager';
import ListWalker from './ListWalker';
const IGNORED_ATTRS = [ "autocomplete", "name" ];
const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~
export default class DOMManager extends ListWalker<Message> {
private isMobile: boolean;
private screen: StatedScreen;
private nl: Array<Node> = [];
private isLink: Array<boolean> = []; // Optimisations
private bodyId: number = -1;
private postponedBodyMessage: CreateElementNode | null = null;
private nodeScrollManagers: Array<ListWalker<SetNodeScroll>> = [];
private stylesManager: StylesManager;
private startTime: number;
constructor(screen: StatedScreen, isMobile: boolean, startTime: number) {
super();
this.startTime = startTime;
this.isMobile = isMobile;
this.screen = screen;
this.stylesManager = new StylesManager(screen);
}
get time(): number {
return this.startTime;
}
append(m: Message): void {
switch (m.tp) {
case "set_node_scroll":
if (!this.nodeScrollManagers[ m.id ]) {
this.nodeScrollManagers[ m.id ] = new ListWalker();
}
this.nodeScrollManagers[ m.id ].append(m);
return;
//case "css_insert_rule": // || //set_css_data ???
//case "css_delete_rule":
// (m.tp === "set_node_attribute" && this.isLink[ m.id ] && m.key === "href")) {
// this.stylesManager.append(m);
// return;
default:
if (m.tp === "create_element_node") {
switch(m.tag) {
case "LINK":
this.isLink[ m.id ] = true;
break;
case "BODY":
this.bodyId = m.id; // Can be several body nodes at one document session?
break;
}
} else if (m.tp === "set_node_attribute" &&
(IGNORED_ATTRS.includes(m.name) || !ATTR_NAME_REGEXP.test(m.name))) {
logger.log("Ignorring message: ", m)
return; // Ignoring...
}
super.append(m);
}
}
private removeBodyScroll(id: number): void {
if (this.isMobile && this.bodyId === id) {
(this.nl[ id ] as HTMLBodyElement).style.overflow = "hidden";
}
}
// May be make it as a message on message add?
private removeAutocomplete({ id, tag }: CreateElementNode): boolean {
const node = this.nl[ id ] as HTMLElement;
if ([ "FORM", "TEXTAREA", "SELECT" ].includes(tag)) {
node.setAttribute("autocomplete", "off");
return true;
}
if (tag === "INPUT") {
node.setAttribute("autocomplete", "new-password");
return true;
}
return false;
}
// type = NodeMessage ?
private insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void {
if (!this.nl[ id ]) {
logger.error("Insert error. Node not found", id);
return;
}
if (!this.nl[ parentID ]) {
logger.error("Insert error. Parent node not found", parentID);
return;
}
// WHAT if text info contains some rules and the ordering is just wrong???
const el = this.nl[ parentID ]
if ((el instanceof HTMLStyleElement) && // TODO: correct ordering OR filter in tracker
el.sheet &&
el.sheet.cssRules &&
el.sheet.cssRules.length > 0 &&
el.innerText.trim().length === 0) {
logger.log("Trying to insert child to a style tag with virtual rules: ", this.nl[ parentID ], this.nl[ id ]);
return;
}
const childNodes = this.nl[ parentID ].childNodes;
if (!childNodes) {
logger.error("Node has no childNodes", this.nl[ parentID ]);
return;
}
if (this.nl[ id ] instanceof HTMLHtmlElement) {
// What if some exotic cases?
this.nl[ parentID ].replaceChild(this.nl[ id ], childNodes[childNodes.length-1])
return
}
this.nl[ parentID ]
.insertBefore(this.nl[ id ], childNodes[ index ])
}
private applyMessage = (msg: Message): void => {
let node;
let doc: Document | null;
switch (msg.tp) {
case "create_document":
doc = this.screen.document;
if (!doc) {
logger.error("No iframe document found", msg)
return;
}
doc.open();
doc.write("<!DOCTYPE html><html></html>");
doc.close();
const fRoot = doc.documentElement;
fRoot.innerText = '';
this.nl = [ fRoot ];
// the last load event I can control
//if (this.document.fonts) {
// this.document.fonts.onloadingerror = () => this.marker.redraw();
// this.document.fonts.onloadingdone = () => this.marker.redraw();
//}
//this.screen.setDisconnected(false);
this.stylesManager.reset();
return
case "create_text_node":
this.nl[ msg.id ] = document.createTextNode('');
this.insertNode(msg);
return
case "create_element_node":
if (msg.svg) {
this.nl[ msg.id ] = document.createElementNS('http://www.w3.org/2000/svg', msg.tag);
} else {
this.nl[ msg.id ] = document.createElement(msg.tag);
}
if (this.bodyId === msg.id) { // there are several bodies in iframes TODO: optimise & cache prebuild
this.postponedBodyMessage = msg;
} else {
this.insertNode(msg);
}
this.removeBodyScroll(msg.id);
this.removeAutocomplete(msg);
return
case "move_node":
this.insertNode(msg);
return
case "remove_node":
node = this.nl[ msg.id ]
if (!node) { logger.error("Node not found", msg); return }
if (!node.parentElement) { logger.error("Parent node not found", msg); return }
node.parentElement.removeChild(node);
return
case "set_node_attribute":
let { id, name, value } = msg;
node = this.nl[ id ];
if (!node) { logger.error("Node not found", msg); return }
if (this.isLink[ id ] && name === "href") {
// @ts-ignore TODO: global ENV type
if (value.startsWith(window.env.ASSETS_HOST || window.location.origin + '/assets')) { // Hack for queries in rewrited urls
value = value.replace("?", "%3F");
}
this.stylesManager.setStyleHandlers(node, value);
}
if (node.namespaceURI === 'http://www.w3.org/2000/svg' && value.startsWith("url(")) {
value = "url(#" + (value.split("#")[1] ||")")
}
try {
node.setAttribute(name, value);
} catch(e) {
logger.error(e, msg);
}
this.removeBodyScroll(msg.id);
return
case "remove_node_attribute":
if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); return }
try {
(this.nl[ msg.id ] as HTMLElement).removeAttribute(msg.name);
} catch(e) {
logger.error(e, msg);
}
return
case "set_input_value":
node = this.nl[ msg.id ]
if (!node) { logger.error("Node not found", msg); return }
if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement)) {
logger.error("Trying to set value of non-Input element", msg)
return
}
const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value
doc = this.screen.document
if (doc && node === doc.activeElement) {
// For the case of Remote Control
node.onblur = () => { node.value = val }
return
}
node.value = val
return
case "set_input_checked":
node = this.nl[ msg.id ];
if (!node) { logger.error("Node not found", msg); return }
(node as HTMLInputElement).checked = msg.checked;
return
case "set_node_data":
case "set_css_data":
node = this.nl[ msg.id ]
if (!node) { logger.error("Node not found", msg); return }
// @ts-ignore
node.data = msg.data;
if (node instanceof HTMLStyleElement) {
doc = this.screen.document
doc && rewriteNodeStyleSheet(doc, node)
}
return
case "css_insert_rule":
node = this.nl[ msg.id ];
if (!node) { logger.error("Node not found", msg); return }
if (!(node instanceof HTMLStyleElement) // link or null
|| node.sheet == null) {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg);
return
}
try {
node.sheet.insertRule(msg.rule, msg.index)
} catch (e) {
logger.warn(e, msg)
try {
node.sheet.insertRule(msg.rule)
} catch (e) {
logger.warn("Cannot insert rule.", e, msg)
}
}
return
case "css_delete_rule":
node = this.nl[ msg.id ];
if (!node) { logger.error("Node not found", msg); return }
if (!(node instanceof HTMLStyleElement) // link or null
|| node.sheet == null) {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg);
return
}
try {
node.sheet.deleteRule(msg.index)
} catch (e) {
logger.warn(e, msg)
}
return
case "create_i_frame_document":
node = this.nl[ msg.frameID ];
// console.log('ifr', msg, node)
if (node instanceof HTMLIFrameElement) {
doc = node.contentDocument;
if (!doc) {
logger.warn("No iframe doc", msg, node, node.contentDocument);
return;
}
this.nl[ msg.id ] = doc.documentElement
return;
} else if (node instanceof Element) { // shadow DOM
try {
this.nl[ msg.id ] = node.attachShadow({ mode: 'open' })
} catch(e) {
logger.warn("Can not attach shadow dom", e, msg)
}
} else {
logger.warn("Context message host is not Element", msg)
}
return
}
}
moveReady(t: number): Promise<void> {
this.moveApply(t, this.applyMessage) // This function autoresets pointer if necessary (better name?)
/* Mount body as late as possible */
if (this.postponedBodyMessage != null) {
this.insertNode(this.postponedBodyMessage)
this.postponedBodyMessage = null
}
// Thinkabout (read): css preload
// What if we go back before it is ready? We'll have two handlres?
return this.stylesManager.moveReady(t).then(() => {
// Apply all scrolls after the styles got applied
this.nodeScrollManagers.forEach(manager => {
const msg = manager.moveGetLast(t)
if (!!msg && !!this.nl[msg.id]) {
const node = this.nl[msg.id] as HTMLElement
node.scrollLeft = msg.x
node.scrollTop = msg.y
}
})
})
}
}

View file

@ -2,7 +2,7 @@ import type StatedScreen from '../StatedScreen';
import type { Message } from '../messages';
import ListWalker from './ListWalker';
import DOMManager from './DOMManager';
import DOMManager from './DOM/DOMManager';
export default class PagesManager extends ListWalker<DOMManager> {

View file

@ -42,5 +42,6 @@ module.exports = {
default: '#DDDDDD',
'gray-light-shade': '#EEEEEE',
'primary': '#3490dc',
'transparent': 'transparent',
}
}

View file

@ -8,8 +8,8 @@ export const issues_types = List([
{ 'type': 'click_rage', 'visible': true, 'order': 2, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' },
{ 'type': 'crash', 'visible': true, 'order': 3, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' },
{ 'type': 'memory', 'visible': true, 'order': 4, 'name': 'High Memory', 'icon': 'funnel/sd-card' },
{ 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' },
{ 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' },
// { 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' },
// { 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' },
// { 'type': 'bad_request', 'visible': true, 'order': 1, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' },
// { 'type': 'missing_resource', 'visible': true, 'order': 2, 'name': 'Missing Images', 'icon': 'funnel/image' },
// { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/dizzy' },
@ -40,7 +40,7 @@ export default Record({
fromJS: ({ type, ...rest }) => ({
...rest,
type,
icon: issues_types_map[type].icon,
name: issues_types_map[type].name,
icon: issues_types_map[type]?.icon,
name: issues_types_map[type]?.name,
}),
});

View file

@ -1,9 +1,11 @@
server {
listen 8080 default_server;
listen [::]:8080 default_server;
root /var/www/openreplay;
index index.html;
location / {
try_files $uri $uri/ =404;
rewrite ^((?!.(js|css|png|svg|jpg|woff|woff2)).)*$ /index.html break;
proxy_intercept_errors on; # see frontend://nginx.org/en/docs/frontend/ngx_frontend_proxy_module.html#proxy_intercept_errors
error_page 404 =200 /index.html;
}
}

View file

@ -96,8 +96,16 @@ spec:
containerPort: {{ $val }}
protocol: TCP
{{- end }}
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -99,3 +99,15 @@ nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -90,8 +90,16 @@ spec:
containerPort: {{ $val }}
protocol: TCP
{{- end }}
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -96,3 +96,15 @@ nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -62,10 +62,18 @@ spec:
{{- range $key, $val := .Values.service.ports }}
- name: {{ $key }}
containerPort: {{ $val }}
protocol: TCP
{{- end }}
protocol: TCP
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -97,3 +97,15 @@ nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -106,8 +106,16 @@ spec:
containerPort: {{ $val }}
protocol: TCP
{{- end }}
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -120,3 +120,15 @@ healthCheck:
initialDelaySeconds: 100
periodSeconds: 15
timeoutSeconds: 10
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -62,8 +62,16 @@ spec:
containerPort: {{ $val }}
protocol: TCP
{{- end }}
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -98,3 +98,15 @@ nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -62,8 +62,16 @@ spec:
containerPort: {{ $val }}
protocol: TCP
{{- end }}
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -97,3 +97,15 @@ nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -60,8 +60,16 @@ spec:
containerPort: {{ $val }}
protocol: TCP
{{- end }}
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -97,3 +97,14 @@ nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -88,8 +88,16 @@ spec:
containerPort: {{ $val }}
protocol: TCP
{{- end }}
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -97,3 +97,15 @@ nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -62,8 +62,16 @@ spec:
containerPort: {{ $val }}
protocol: TCP
{{- end }}
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -98,3 +98,15 @@ nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -54,8 +54,16 @@ spec:
containerPort: {{ $val }}
protocol: TCP
{{- end }}
{{- with .Values.persistence.mounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.persistence.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}

View file

@ -93,3 +93,15 @@ nodeSelector: {}
tolerations: []
affinity: {}
persistence: {}
# # Spec of spec.template.spec.containers[*].volumeMounts
# mounts:
# - name: kafka-ssl
# mountPath: /opt/kafka/ssl
# # Spec of spec.template.spec.volumes
# volumes:
# - name: kafka-ssl
# secret:
# secretName: kafka-ssl

View file

@ -103,6 +103,7 @@ spec:
mountPath: /opt/openreplay
- name: dbmigrationscript
mountPath: /opt/migrations/
{{- if eq .Values.global.s3.endpoint "http://minio.db.svc.cluster.local:9000" }}
- name: minio
image: bitnami/minio:2020.10.9-debian-10-r6
env:
@ -128,6 +129,7 @@ spec:
mountPath: /opt/openreplay
- name: dbmigrationscript
mountPath: /opt/migrations/
{{- end}}
{{- if .Values.global.enterpriseEditionLicense }}
# Enterprise migration
- name: clickhouse

View file

@ -60,8 +60,8 @@ type AppOptions = {
__is_snippet: boolean;
__debug_report_edp: string | null;
__debug__?: LoggerOptions;
localStorage: Storage;
sessionStorage: Storage;
localStorage: Storage | null;
sessionStorage: Storage | null;
// @deprecated
onStart?: StartCallback;
@ -115,8 +115,8 @@ export default class App {
verbose: false,
__is_snippet: false,
__debug_report_edp: null,
localStorage: window.localStorage,
sessionStorage: window.sessionStorage,
localStorage: null,
sessionStorage: null,
},
options,
);
@ -139,8 +139,10 @@ export default class App {
Object.entries(metadata).forEach(([key, value]) => this.send(new Metadata(key, value)));
}
});
this.localStorage = this.options.localStorage;
this.sessionStorage = this.options.sessionStorage;
// window.localStorage and window.sessionStorage should only be accessed if required, see #490, #637
this.localStorage = this.options.localStorage ?? window.localStorage;
this.sessionStorage = this.options.sessionStorage ?? window.sessionStorage;
if (sessionToken != null) {
this.sessionStorage.setItem(this.options.session_token_key, sessionToken);