Compare commits
12 commits
main
...
heuristics
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
684399ff1a | ||
|
|
f51cf5ccf2 | ||
|
|
daa4489057 | ||
|
|
37fdc04693 | ||
|
|
ff1cd1e01f | ||
|
|
80aca09560 | ||
|
|
88005ec0d0 | ||
|
|
af96e5bc26 | ||
|
|
ebc633d147 | ||
|
|
c5c7764212 | ||
|
|
0fb7e4bd07 | ||
|
|
be7c63150b |
52 changed files with 2377 additions and 484 deletions
2
.github/workflows/api.yaml
vendored
2
.github/workflows/api.yaml
vendored
|
|
@ -43,6 +43,8 @@ jobs:
|
|||
- name: Deploy to kubernetes
|
||||
run: |
|
||||
cd scripts/helm/
|
||||
sed -i "s#minio_access_key.*#minio_access_key: \"${{ secrets.OSS_MINIO_ACCESS_KEY }}\" #g" vars.yaml
|
||||
sed -i "s#minio_secret_key.*#minio_secret_key: \"${{ secrets.OSS_MINIO_SECRET_KEY }}\" #g" vars.yaml
|
||||
sed -i "s#domain_name.*#domain_name: \"foss.openreplay.com\" #g" vars.yaml
|
||||
sed -i "s#kubeconfig.*#kubeconfig_path: ${KUBECONFIG}#g" vars.yaml
|
||||
sed -i "s/tag:.*/tag: \"$IMAGE_TAG\"/g" app/chalice.yaml
|
||||
|
|
|
|||
5
.github/workflows/workers.yaml
vendored
5
.github/workflows/workers.yaml
vendored
|
|
@ -64,9 +64,12 @@ jobs:
|
|||
# Deploying image to environment.
|
||||
#
|
||||
cd ../scripts/helm/
|
||||
sed -i "s#minio_access_key.*#minio_access_key: \"${{ secrets.OSS_MINIO_ACCESS_KEY }}\" #g" vars.yaml
|
||||
sed -i "s#minio_secret_key.*#minio_secret_key: \"${{ secrets.OSS_MINIO_SECRET_KEY }}\" #g" vars.yaml
|
||||
sed -i "s#domain_name.*#domain_name: \"foss.openreplay.com\" #g" vars.yaml
|
||||
sed -i "s#kubeconfig.*#kubeconfig_path: ${KUBECONFIG}#g" vars.yaml
|
||||
for image in $(cat ../../backend/images_to_build.txt);
|
||||
do
|
||||
sed -i "s#kubeconfig.*#kubeconfig_path: ${KUBECONFIG}#g" vars.yaml
|
||||
sed -i "s/tag:.*/tag: \"$IMAGE_TAG\"/g" app/${image}.yaml
|
||||
# Deploy command
|
||||
bash kube-install.sh --app $image
|
||||
|
|
|
|||
1
api/.gitignore
vendored
1
api/.gitignore
vendored
|
|
@ -83,6 +83,7 @@ wheels/
|
|||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
Pipfile
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from sentry_sdk import configure_scope
|
|||
|
||||
from chalicelib import _overrides
|
||||
from chalicelib.blueprints import bp_authorizers
|
||||
from chalicelib.blueprints import bp_core, bp_core_crons
|
||||
from chalicelib.blueprints import bp_core, bp_core_crons, bp_app_api
|
||||
from chalicelib.blueprints import bp_core_dynamic, bp_core_dynamic_crons
|
||||
from chalicelib.blueprints.subs import bp_dashboard
|
||||
from chalicelib.utils import helper
|
||||
|
|
@ -99,3 +99,5 @@ app.register_blueprint(bp_core_crons.app)
|
|||
app.register_blueprint(bp_core_dynamic.app)
|
||||
app.register_blueprint(bp_core_dynamic_crons.app)
|
||||
app.register_blueprint(bp_dashboard.app)
|
||||
app.register_blueprint(bp_app_api.app)
|
||||
|
||||
|
|
|
|||
55
api/chalicelib/blueprints/bp_app_api.py
Normal file
55
api/chalicelib/blueprints/bp_app_api.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
from chalice import Blueprint
|
||||
|
||||
from chalicelib import _overrides
|
||||
from chalicelib.blueprints import bp_authorizers
|
||||
from chalicelib.core import sessions
|
||||
|
||||
app = Blueprint(__name__)
|
||||
_overrides.chalice_app(app)
|
||||
|
||||
|
||||
@app.route('/app/{projectId}/users/{userId}/sessions', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_user_sessions2(projectId, userId, context):
|
||||
params = app.current_request.query_params
|
||||
|
||||
if params is None:
|
||||
params = {}
|
||||
|
||||
return {
|
||||
'data': sessions.get_user_sessions(
|
||||
project_id=projectId,
|
||||
user_id=userId,
|
||||
start_date=params.get('start_date'),
|
||||
end_date=params.get('end_date')
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@app.route('/app/{projectId}/users/{userId}/events', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_user_events():
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/app/{projectId}/users/{userId}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_user_details():
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/app/{projectId}/users/{userId}', methods=['DELETE'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def delete_user_data():
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/app/{projectId}/jobs', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_jobs():
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/app/{projectId}/jobs/{jobId}', methods=['GET'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def get_job():
|
||||
pass
|
||||
|
||||
|
||||
@app.route('/app/{projectId}/jobs/{jobId}', methods=['DELETE'], authorizer=bp_authorizers.api_key_authorizer)
|
||||
def cancel_job():
|
||||
pass
|
||||
|
|
@ -617,3 +617,43 @@ def get_favorite_sessions(project_id, user_id, include_viewed=False):
|
|||
|
||||
sessions = cur.fetchall()
|
||||
return helper.list_to_camel_case(sessions)
|
||||
|
||||
|
||||
def get_user_sessions(project_id, user_id, start_date, end_date):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
constraints = ["s.project_id = %(projectId)s", "s.user_id = %(userId)s"]
|
||||
if start_date is not None:
|
||||
constraints.append("s.start_ts >= %(startDate)s")
|
||||
if end_date is not None:
|
||||
constraints.append("s.start_ts <= %(endDate)s")
|
||||
|
||||
query_part = f"""\
|
||||
FROM public.sessions AS s
|
||||
WHERE {" AND ".join(constraints)}"""
|
||||
|
||||
cur.execute(cur.mogrify(f"""\
|
||||
SELECT s.project_id,
|
||||
s.session_id::text AS session_id,
|
||||
s.user_uuid,
|
||||
s.user_id,
|
||||
s.user_agent,
|
||||
s.user_os,
|
||||
s.user_browser,
|
||||
s.user_device,
|
||||
s.user_country,
|
||||
s.start_ts,
|
||||
s.duration,
|
||||
s.events_count,
|
||||
s.pages_count,
|
||||
s.errors_count
|
||||
{query_part}
|
||||
ORDER BY s.session_id
|
||||
LIMIT 50;""", {
|
||||
"projectId": project_id,
|
||||
"userId": user_id,
|
||||
"startDate": start_date,
|
||||
"endDate": end_date
|
||||
}))
|
||||
|
||||
sessions = cur.fetchall()
|
||||
return helper.list_to_camel_case(sessions)
|
||||
|
|
|
|||
|
|
@ -3,19 +3,20 @@ module openreplay/backend
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/clickhouse-go v1.4.3
|
||||
github.com/aws/aws-sdk-go v1.35.23
|
||||
github.com/btcsuite/btcutil v1.0.2
|
||||
github.com/confluentinc/confluent-kafka-go v1.5.2 // indirect
|
||||
github.com/go-redis/redis v6.15.9+incompatible
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/jackc/pgconn v1.6.0
|
||||
github.com/jackc/pgx/v4 v4.6.0
|
||||
github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451
|
||||
github.com/jackc/pgx/v4 v4.6.0
|
||||
github.com/klauspost/compress v1.11.9
|
||||
github.com/klauspost/pgzip v1.2.5
|
||||
github.com/oschwald/maxminddb-golang v1.7.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
|
||||
github.com/ua-parser/uap-go v0.0.0-20200325213135-e1c09f13e2fe
|
||||
gopkg.in/confluentinc/confluent-kafka-go.v1 v1.5.2
|
||||
github.com/ClickHouse/clickhouse-go v1.4.3
|
||||
github.com/aws/aws-sdk-go v1.35.23
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/aws/aws-sdk-go v1.35.23 h1:SCP0d0XvyJTDmfnHEQPvBaYi3kea1VNUo7uQmkVgFts=
|
||||
github.com/aws/aws-sdk-go v1.35.23/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
|
||||
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=
|
||||
|
|
@ -24,6 +25,7 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
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=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
|
|
@ -73,6 +75,7 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
|
|||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
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=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
|
|
@ -103,6 +106,7 @@ github.com/oschwald/maxminddb-golang v1.7.0 h1:JmU4Q1WBv5Q+2KZy5xJI+98aUwTIrPPxZ
|
|||
github.com/oschwald/maxminddb-golang v1.7.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
|
|
|
|||
|
|
@ -157,7 +157,11 @@ func (a *Alert) CanCheck() bool {
|
|||
}
|
||||
|
||||
func (a *Alert) Build() (sq.SelectBuilder, error) {
|
||||
colDef := LeftToDb[a.Query.Left]
|
||||
colDef, ok := LeftToDb[a.Query.Left]
|
||||
if !ok {
|
||||
return sq.Select(), errors.New(fmt.Sprintf("!! unsupported metric '%s' from alert: %d:%s\n", a.Query.Left, a.AlertID, a.Name))
|
||||
}
|
||||
|
||||
subQ := sq.
|
||||
Select(colDef.formula + " AS value").
|
||||
From(colDef.table).
|
||||
|
|
@ -221,4 +225,4 @@ func (a *Alert) Build() (sq.SelectBuilder, error) {
|
|||
return q, errors.New("unsupported detection method")
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
}
|
||||
255
backend/services/detectors/builder/builder.go
Normal file
255
backend/services/detectors/builder/builder.go
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"log"
|
||||
"openreplay/backend/pkg/intervals"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
type ReducedHandler struct {
|
||||
handlers []handler
|
||||
}
|
||||
|
||||
func (rh *ReducedHandler) HandleMessage(msg Message) []Message {
|
||||
var resultMessages []Message
|
||||
for _, h := range rh.handlers {
|
||||
resultMessages = append(resultMessages, h.HandleMessage(msg)...)
|
||||
}
|
||||
return resultMessages
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
type CombinedHandler struct {
|
||||
handlers []handler
|
||||
}
|
||||
|
||||
func (ch *CombinedHandler) HandleMessage(msg Message) []Message {
|
||||
resultMessages := []Message{msg}
|
||||
for _, h := range ch.handlers {
|
||||
var nextResultMessages []Message
|
||||
for _, m := resultMessages {
|
||||
nextResultMessages = append(nextResultMessages, h.HandleMessage(m)...)
|
||||
}
|
||||
resultMessages = nextResultMessages
|
||||
}
|
||||
return resultMessages
|
||||
}
|
||||
|
||||
type builder struct {
|
||||
readyMessages []Message // a collection of built events
|
||||
timestamp uint64 // current timestamp
|
||||
peBuilder *pageEventBuilder
|
||||
ptaBuilder *performanceTrackAggrBuilder
|
||||
ieBuilder *inputEventBuilder
|
||||
reBuilder *resourceEventBuilder
|
||||
ciDetector *cpuIssueDetector
|
||||
miDetector *memoryIssueDetector
|
||||
ddDetector *domDropDetector
|
||||
crDetector *clickRageDetector
|
||||
crshDetector *crashDetector
|
||||
dcDetector *deadClickDetector
|
||||
qrdetector *quickReturnDetector
|
||||
ridetector *repetitiveInputDetector
|
||||
exsdetector *excessiveScrollingDetector
|
||||
chdetector *clickHesitationDetector
|
||||
integrationsWaiting bool
|
||||
sid uint64
|
||||
sessionEventsCache []*IssueEvent // a cache of selected ready messages used to detect events that depend on other events
|
||||
}
|
||||
|
||||
func NewBuilder() *builder {
|
||||
return &builder{
|
||||
peBuilder: &pageEventBuilder{},
|
||||
ptaBuilder: &performanceTrackAggrBuilder{},
|
||||
ieBuilder: NewInputEventBuilder(),
|
||||
reBuilder: &resourceEventBuilder{},
|
||||
ciDetector: &cpuIssueDetector{},
|
||||
miDetector: &memoryIssueDetector{},
|
||||
ddDetector: &domDropDetector{},
|
||||
crDetector: &clickRageDetector{},
|
||||
crshDetector: &crashDetector{},
|
||||
dcDetector: &deadClickDetector{},
|
||||
qrdetector: &quickReturnDetector{},
|
||||
ridetector: &repetitiveInputDetector{},
|
||||
exsdetector: &excessiveScrollingDetector{},
|
||||
chdetector: &clickHesitationDetector{},
|
||||
integrationsWaiting: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Additional methods for builder
|
||||
func (b *builder) appendReadyMessage(msg Message) { // interface is never nil even if it holds nil value
|
||||
b.readyMessages = append(b.readyMessages, msg)
|
||||
}
|
||||
func (b *builder) appendSessionEvent(msg *IssueEvent) { // interface is never nil even if it holds nil value
|
||||
b.sessionEventsCache = append(b.sessionEventsCache, msg)
|
||||
}
|
||||
|
||||
func (b *builder) iterateReadyMessage(iter func(msg Message)) {
|
||||
for _, readyMsg := range b.readyMessages {
|
||||
iter(readyMsg)
|
||||
}
|
||||
b.readyMessages = nil
|
||||
}
|
||||
|
||||
func (b *builder) buildSessionEnd() {
|
||||
sessionEnd := &SessionEnd{
|
||||
Timestamp: b.timestamp, // + delay?
|
||||
}
|
||||
b.appendReadyMessage(sessionEnd)
|
||||
}
|
||||
|
||||
// ==================== DETECTORS ====================
|
||||
|
||||
func (b *builder) detectCpuIssue(msg Message, messageID uint64) {
|
||||
// handle message and append to ready messages if it's fully composed
|
||||
if rm := b.ciDetector.HandleMessage(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) detectMemoryIssue(msg Message, messageID uint64) {
|
||||
// handle message and append to ready messages if it's fully composed
|
||||
if rm := b.miDetector.HandleMessage(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) detectDomDrop(msg Message) {
|
||||
// handle message and append to ready messages if it's fully composed
|
||||
if dd := b.ddDetector.HandleMessage(msg, b.timestamp); dd != nil {
|
||||
b.appendSessionEvent(dd) // not to ready messages, since we don't put it as anomaly
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) detectDeadClick(msg Message, messageID uint64) {
|
||||
if rm := b.dcDetector.HandleMessage(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
}
|
||||
func (b *builder) detectQuickReturn(msg Message, messageID uint64) {
|
||||
if rm := b.qrdetector.HandleMessage(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
}
|
||||
func (b *builder) detectClickHesitation(msg Message, messageID uint64) {
|
||||
if rm := b.chdetector.HandleMessage(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
}
|
||||
func (b *builder) detectRepetitiveInput(msg Message, messageID uint64) {
|
||||
if rm := b.ridetector.HandleMessage(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
}
|
||||
func (b *builder) detectExcessiveScrolling(msg Message, messageID uint64) {
|
||||
if rm := b.exsdetector.HandleMessage(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== BUILDERS ====================
|
||||
|
||||
func (b *builder) handlePerformanceTrackAggr(message Message, messageID uint64) {
|
||||
if msg := b.ptaBuilder.HandleMessage(message, messageID, b.timestamp); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
func (b *builder) buildPerformanceTrackAggr() {
|
||||
if msg := b.ptaBuilder.Build(); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) handleInputEvent(message Message, messageID uint64) {
|
||||
if msg := b.ieBuilder.HandleMessage(message, messageID, b.timestamp); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
func (b *builder) buildInputEvent() {
|
||||
if msg := b.ieBuilder.Build(); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) handlePageEvent(message Message, messageID uint64) {
|
||||
if msg := b.peBuilder.HandleMessage(message, messageID, b.timestamp); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
func (b *builder) buildPageEvent() {
|
||||
if msg := b.peBuilder.Build(); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) handleResourceEvent(message Message, messageID uint64) {
|
||||
if msg := b.reBuilder.HandleMessage(message, messageID); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) handleMessage(message Message, messageID uint64) {
|
||||
|
||||
// update current timestamp
|
||||
switch msg := message.(type) {
|
||||
//case *SessionDisconnect:
|
||||
// b.timestamp = msg.Timestamp
|
||||
case *SessionStart:
|
||||
b.timestamp = msg.Timestamp
|
||||
case *Timestamp:
|
||||
b.timestamp = msg.Timestamp
|
||||
}
|
||||
// Start only from the first timestamp event.
|
||||
if b.timestamp == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Pass message to detector handlers
|
||||
b.detectCpuIssue(message, messageID)
|
||||
b.detectMemoryIssue(message, messageID)
|
||||
b.detectDomDrop(message)
|
||||
b.detectDeadClick(message, messageID)
|
||||
b.detectQuickReturn(message, messageID)
|
||||
b.detectClickHesitation(message, messageID)
|
||||
b.detectRepetitiveInput(message, messageID)
|
||||
b.detectExcessiveScrolling(message, messageID)
|
||||
|
||||
// Pass message to eventBuilders handler
|
||||
b.handleInputEvent(message, messageID)
|
||||
b.handlePageEvent(message, messageID)
|
||||
b.handlePerformanceTrackAggr(message, messageID)
|
||||
b.handleResourceEvent(message, messageID)
|
||||
|
||||
// Handle messages which translate to events without additional operations
|
||||
b.HandleSimpleMessages(message, messageID)
|
||||
}
|
||||
|
||||
func (b *builder) buildEvents(ts int64) {
|
||||
if b.timestamp == 0 {
|
||||
return // There was no timestamp events yet
|
||||
}
|
||||
|
||||
if b.peBuilder.HasInstance() && int64(b.peBuilder.GetTimestamp())+intervals.EVENTS_PAGE_EVENT_TIMEOUT < ts {
|
||||
b.buildPageEvent()
|
||||
}
|
||||
if b.ieBuilder.HasInstance() && int64(b.ieBuilder.GetTimestamp())+intervals.EVENTS_INPUT_EVENT_TIMEOUT < ts {
|
||||
b.buildInputEvent()
|
||||
}
|
||||
if b.ptaBuilder.HasInstance() && int64(b.ptaBuilder.GetStartTimestamp())+intervals.EVENTS_PERFORMANCE_AGGREGATION_TIMEOUT < ts {
|
||||
b.buildPerformanceTrackAggr()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) checkTimeouts(ts int64) bool {
|
||||
if b.timestamp == 0 {
|
||||
return false // There was no timestamp events yet
|
||||
}
|
||||
lastTsGap := ts - int64(b.timestamp)
|
||||
log.Printf("checking timeouts for sess %v: %v now, %v sesstime; gap %v", b.sid, ts, b.timestamp, lastTsGap)
|
||||
if lastTsGap > intervals.EVENTS_SESSION_END_TIMEOUT {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
51
backend/services/detectors/builder/builderMap.go
Normal file
51
backend/services/detectors/builder/builderMap.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
type builderMap map[uint64]*builder
|
||||
|
||||
func NewBuilderMap() builderMap {
|
||||
return make(builderMap)
|
||||
}
|
||||
|
||||
func (m builderMap) getBuilder(sessionID uint64) *builder {
|
||||
b := m[sessionID]
|
||||
if b == nil {
|
||||
b = NewBuilder()
|
||||
m[sessionID] = b
|
||||
b.sid = sessionID
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (m builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint64) {
|
||||
b := m.getBuilder(sessionID)
|
||||
b.handleMessage(msg, messageID)
|
||||
}
|
||||
|
||||
func (m builderMap) IterateSessionReadyMessages(sessionID uint64, operatingTs int64, iter func(msg Message)) {
|
||||
b, ok := m[sessionID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
b.buildEvents(operatingTs)
|
||||
sessionEnded := b.checkTimeouts(operatingTs)
|
||||
b.iterateReadyMessage(iter)
|
||||
if sessionEnded {
|
||||
delete(m, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func (m builderMap) IterateReadyMessages(operatingTs int64, iter func(sessionID uint64, msg Message)) {
|
||||
for sessionID, b := range m {
|
||||
sessionEnded := b.checkTimeouts(operatingTs)
|
||||
b.iterateReadyMessage(func(msg Message) {
|
||||
iter(sessionID, msg)
|
||||
})
|
||||
if sessionEnded {
|
||||
delete(m, sessionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
const HESITATION_THRESHOLD = 3000 // ms
|
||||
|
||||
type clickHesitationDetector struct{}
|
||||
|
||||
func (chd *clickHesitationDetector) HandleMouseClick(msg *MouseClick, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
if msg.HesitationTime > HESITATION_THRESHOLD {
|
||||
return &IssueEvent{
|
||||
Timestamp: timestamp,
|
||||
MessageID: messageID,
|
||||
Type: "click_hesitation",
|
||||
Context: msg.Label,
|
||||
ContextString: "Click hesitation above 3 seconds",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (chd *clickHesitationDetector) HandleMessage(message Message, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
switch msg := message.(type) {
|
||||
case *MouseClick:
|
||||
return chd.HandleMouseClick(msg, messageID, timestamp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,34 +1,42 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/json"
|
||||
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
|
||||
const CLICK_TIME_DIFF = 200
|
||||
const MIN_CLICKS_IN_A_ROW = 3
|
||||
|
||||
type clickRageDetector struct {
|
||||
lastTimestamp uint64
|
||||
lastLabel string
|
||||
lastTimestamp uint64
|
||||
lastLabel string
|
||||
firstInARawTimestamp uint64
|
||||
firstInARawMessageId uint64
|
||||
countsInARow int
|
||||
countsInARow int
|
||||
}
|
||||
|
||||
func (crd *clickRageDetector) HandleMessage(message Message, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
switch msg := message.(type) {
|
||||
case *MouseClick:
|
||||
return crd.HandleMouseClick(msg, messageID, timestamp)
|
||||
case *SessionEnd:
|
||||
return crd.Build()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (crd *clickRageDetector) Build() *IssueEvent {
|
||||
var i *IssueEvent
|
||||
if crd.countsInARow >= MIN_CLICKS_IN_A_ROW {
|
||||
payload, _ := json.Marshal(struct{Count int }{crd.countsInARow,})
|
||||
payload, _ := json.Marshal(struct{ Count int }{crd.countsInARow})
|
||||
i = &IssueEvent{
|
||||
Type: "click_rage",
|
||||
Type: "click_rage",
|
||||
ContextString: crd.lastLabel,
|
||||
Payload: string(payload), // TODO: json encoder
|
||||
Timestamp: crd.firstInARawTimestamp,
|
||||
MessageID: crd.firstInARawMessageId,
|
||||
Payload: string(payload), // TODO: json encoder
|
||||
Timestamp: crd.firstInARawTimestamp,
|
||||
MessageID: crd.firstInARawMessageId,
|
||||
}
|
||||
}
|
||||
crd.lastTimestamp = 0
|
||||
|
|
@ -39,8 +47,8 @@ func (crd *clickRageDetector) Build() *IssueEvent {
|
|||
return i
|
||||
}
|
||||
|
||||
func (crd *clickRageDetector) HandleMouseClick(msg *MouseClick, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
if crd.lastTimestamp + CLICK_TIME_DIFF < timestamp && crd.lastLabel == msg.Label {
|
||||
func (crd *clickRageDetector) HandleMouseClick(msg *MouseClick, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
if crd.lastTimestamp+CLICK_TIME_DIFF < timestamp && crd.lastLabel == msg.Label {
|
||||
crd.lastTimestamp = timestamp
|
||||
crd.countsInARow += 1
|
||||
return nil
|
||||
|
|
@ -54,4 +62,4 @@ func (crd *clickRageDetector) HandleMouseClick(msg *MouseClick, messageID uint6
|
|||
crd.countsInARow = 1
|
||||
}
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
|
@ -3,23 +3,34 @@ package builder
|
|||
import (
|
||||
"encoding/json"
|
||||
|
||||
"openreplay/backend/pkg/messages/performance"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
"openreplay/backend/pkg/messages/performance"
|
||||
)
|
||||
|
||||
const CPU_THRESHOLD = 70 // % out of 100
|
||||
const CPU_THRESHOLD = 70 // % out of 100
|
||||
const CPU_MIN_DURATION_TRIGGER = 6 * 1000
|
||||
|
||||
|
||||
type cpuIssueFinder struct {
|
||||
type cpuIssueDetector struct {
|
||||
startTimestamp uint64
|
||||
startMessageID uint64
|
||||
lastTimestamp uint64
|
||||
maxRate uint64
|
||||
contextString string
|
||||
lastTimestamp uint64
|
||||
maxRate uint64
|
||||
contextString string
|
||||
}
|
||||
|
||||
func (f *cpuIssueFinder) Build() *IssueEvent {
|
||||
func (f *cpuIssueDetector) HandleMessage(message Message, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
switch msg := message.(type) {
|
||||
case *SetPageLocation:
|
||||
f.HandleSetPageLocation(msg)
|
||||
case *PerformanceTrack:
|
||||
return f.HandlePerformanceTrack(msg, messageID, timestamp);
|
||||
case *SessionEnd:
|
||||
return f.Build()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *cpuIssueDetector) Build() *IssueEvent {
|
||||
if f.startTimestamp == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -35,26 +46,24 @@ func (f *cpuIssueFinder) Build() *IssueEvent {
|
|||
return nil
|
||||
}
|
||||
|
||||
payload, _ := json.Marshal(struct{
|
||||
payload, _ := json.Marshal(struct {
|
||||
Duration uint64
|
||||
Rate uint64
|
||||
}{duration,maxRate})
|
||||
Rate uint64
|
||||
}{duration, maxRate})
|
||||
return &IssueEvent{
|
||||
Type: "cpu",
|
||||
Timestamp: timestamp,
|
||||
MessageID: messageID,
|
||||
Type: "cpu",
|
||||
Timestamp: timestamp,
|
||||
MessageID: messageID,
|
||||
ContextString: f.contextString,
|
||||
Payload: string(payload),
|
||||
Payload: string(payload),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *cpuIssueFinder) HandleSetPageLocation(msg *SetPageLocation) {
|
||||
func (f *cpuIssueDetector) HandleSetPageLocation(msg *SetPageLocation) {
|
||||
f.contextString = msg.URL
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (f *cpuIssueFinder) HandlePerformanceTrack(msg *PerformanceTrack, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
func (f *cpuIssueDetector) HandlePerformanceTrack(msg *PerformanceTrack, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
dt := performance.TimeDiff(timestamp, f.lastTimestamp)
|
||||
if dt == 0 {
|
||||
return nil // TODO: handle error
|
||||
|
|
@ -83,4 +92,3 @@ func (f *cpuIssueFinder) HandlePerformanceTrack(msg *PerformanceTrack, messageID
|
|||
return nil
|
||||
}
|
||||
|
||||
|
||||
71
backend/services/detectors/builder/crashDetector.go
Normal file
71
backend/services/detectors/builder/crashDetector.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
const CPU_ISSUE_WINDOW = 3000
|
||||
const MEM_ISSUE_WINDOW = 4 * 1000
|
||||
const DOM_DROP_WINDOW = 200
|
||||
//const CLICK_RELATION_DISTANCE = 1200
|
||||
|
||||
type crashDetector struct {
|
||||
startTimestamp uint64
|
||||
}
|
||||
|
||||
func (*crashDetector) buildCrashEvent(s []*IssueEvent) *IssueEvent{
|
||||
var cpuIssues []*IssueEvent
|
||||
var memIssues []*IssueEvent
|
||||
var domDrops []*IssueEvent
|
||||
for _, e := range s {
|
||||
if e.Type == "cpu" {
|
||||
cpuIssues = append(cpuIssues, e)
|
||||
}
|
||||
if e.Type == "memory" {
|
||||
memIssues = append(memIssues, e)
|
||||
}
|
||||
if e.Type == "dom_drop" {
|
||||
domDrops = append(domDrops, e)
|
||||
}
|
||||
}
|
||||
var i, j, k int
|
||||
for _, e := range s {
|
||||
for i < len(cpuIssues) && cpuIssues[i].Timestamp+CPU_ISSUE_WINDOW < e.Timestamp {
|
||||
i++
|
||||
}
|
||||
for j < len(memIssues) && memIssues[j].Timestamp+MEM_ISSUE_WINDOW < e.Timestamp {
|
||||
j++
|
||||
}
|
||||
for k < len(domDrops) && domDrops[k].Timestamp+DOM_DROP_WINDOW < e.Timestamp { //Actually different type of issue
|
||||
k++
|
||||
}
|
||||
if i == len(cpuIssues) && j == len(memIssues) && k == len(domDrops) {
|
||||
break
|
||||
}
|
||||
if (i < len(cpuIssues) && cpuIssues[i].Timestamp < e.Timestamp+CPU_ISSUE_WINDOW) ||
|
||||
(j < len(memIssues) && memIssues[j].Timestamp < e.Timestamp+MEM_ISSUE_WINDOW) ||
|
||||
(k < len(domDrops) && domDrops[k].Timestamp < e.Timestamp+DOM_DROP_WINDOW) {
|
||||
contextString := "UNKNOWN"
|
||||
return &IssueEvent{
|
||||
MessageID: e.MessageID,
|
||||
Timestamp: e.Timestamp,
|
||||
Type: "crash",
|
||||
ContextString: contextString,
|
||||
Context: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr *crashDetector) HandleMessage(message Message, s []*IssueEvent) *IssueEvent{
|
||||
// Only several message types can trigger crash
|
||||
// which is by our definition, a combination of DomDrop event, CPU and Memory issues
|
||||
switch message.(type) {
|
||||
case *SessionEnd:
|
||||
return cr.buildCrashEvent(s)
|
||||
case *PerformanceTrack:
|
||||
return cr.buildCrashEvent(s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -4,24 +4,22 @@ import (
|
|||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
|
||||
const CLICK_RELATION_TIME = 1400
|
||||
|
||||
type deadClickDetector struct {
|
||||
lastMouseClick *MouseClick
|
||||
lastTimestamp uint64
|
||||
lastMessageID uint64
|
||||
lastMouseClick *MouseClick
|
||||
lastTimestamp uint64
|
||||
lastMessageID uint64
|
||||
}
|
||||
|
||||
|
||||
func (d *deadClickDetector) HandleReaction(timestamp uint64) *IssueEvent {
|
||||
var i *IssueEvent
|
||||
if d.lastMouseClick != nil && d.lastTimestamp + CLICK_RELATION_TIME < timestamp {
|
||||
if d.lastMouseClick != nil && d.lastTimestamp+CLICK_RELATION_TIME < timestamp {
|
||||
i = &IssueEvent{
|
||||
Type: "dead_click",
|
||||
Type: "dead_click",
|
||||
ContextString: d.lastMouseClick.Label,
|
||||
Timestamp: d.lastTimestamp,
|
||||
MessageID: d.lastMessageID,
|
||||
Timestamp: d.lastTimestamp,
|
||||
MessageID: d.lastMessageID,
|
||||
}
|
||||
}
|
||||
d.lastMouseClick = nil
|
||||
|
|
@ -38,8 +36,8 @@ func (d *deadClickDetector) HandleMessage(msg Message, messageID uint64, timesta
|
|||
d.lastMouseClick = m
|
||||
d.lastTimestamp = timestamp
|
||||
d.lastMessageID = messageID
|
||||
case *SetNodeAttribute,
|
||||
*RemoveNodeAttribute,
|
||||
case *SetNodeAttribute,
|
||||
*RemoveNodeAttribute,
|
||||
*CreateElementNode,
|
||||
*CreateTextNode,
|
||||
*MoveNode,
|
||||
|
|
@ -51,5 +49,3 @@ func (d *deadClickDetector) HandleMessage(msg Message, messageID uint64, timesta
|
|||
}
|
||||
return i
|
||||
}
|
||||
|
||||
|
||||
52
backend/services/detectors/builder/domDropDetector.go
Normal file
52
backend/services/detectors/builder/domDropDetector.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
type domDropDetector struct {
|
||||
removedCount int
|
||||
lastDropTimestamp uint64
|
||||
}
|
||||
|
||||
const DROP_WINDOW = 200 //ms
|
||||
const CRITICAL_COUNT = 1 // Our login page contains 20. But on crush it removes only roots (1-3 nodes).
|
||||
|
||||
func (f *domDropDetector) HandleMessage(message Message, timestamp uint64) *IssueEvent {
|
||||
switch message.(type) {
|
||||
case *CreateElementNode,
|
||||
*CreateTextNode:
|
||||
f.HandleNodeCreation()
|
||||
case *RemoveNode:
|
||||
f.HandleNodeRemoval(timestamp)
|
||||
case *CreateDocument:
|
||||
return f.Build()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dd *domDropDetector) HandleNodeCreation() {
|
||||
dd.removedCount = 0
|
||||
dd.lastDropTimestamp = 0
|
||||
}
|
||||
|
||||
func (dd *domDropDetector) HandleNodeRemoval(ts uint64) {
|
||||
if dd.lastDropTimestamp+DROP_WINDOW > ts {
|
||||
dd.removedCount += 1
|
||||
} else {
|
||||
dd.removedCount = 1
|
||||
}
|
||||
dd.lastDropTimestamp = ts
|
||||
}
|
||||
|
||||
func (dd *domDropDetector) Build() *IssueEvent {
|
||||
var domDrop *IssueEvent
|
||||
if dd.removedCount >= CRITICAL_COUNT {
|
||||
domDrop = &IssueEvent{
|
||||
Type: "dom_drop",
|
||||
Timestamp: dd.lastDropTimestamp}
|
||||
}
|
||||
dd.removedCount = 0
|
||||
dd.lastDropTimestamp = 0
|
||||
return domDrop
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
const MAX_SCROLL_PER_PAGE = 20
|
||||
|
||||
type excessiveScrollingDetector struct {
|
||||
currentPage string
|
||||
scrollsNumber uint64
|
||||
}
|
||||
|
||||
func (exs *excessiveScrollingDetector) HandleMouseClick() {
|
||||
exs.scrollsNumber = 0
|
||||
}
|
||||
|
||||
func (exs *excessiveScrollingDetector) HandleSetPageLocation(msg *SetPageLocation) {
|
||||
if msg.Referrer != exs.currentPage {
|
||||
exs.currentPage = msg.Referrer
|
||||
exs.scrollsNumber = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (exs *excessiveScrollingDetector) HandleScroll(msg *SetViewportScroll, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
if exs.scrollsNumber+1 >= MAX_SCROLL_PER_PAGE {
|
||||
return &IssueEvent{
|
||||
MessageID: messageID,
|
||||
Type: "excessive_scrolling",
|
||||
Timestamp: timestamp,
|
||||
ContextString: "Number of scrolling per page is above the threshold",
|
||||
Context: exs.currentPage,
|
||||
}
|
||||
} else {
|
||||
exs.scrollsNumber += 1
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (exs *excessiveScrollingDetector) HandleMessage(message Message, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
switch msg := message.(type) {
|
||||
case *MouseClick:
|
||||
exs.HandleMouseClick()
|
||||
case *SetPageLocation:
|
||||
if msg.NavigationStart != 0 {
|
||||
exs.HandleSetPageLocation(msg)
|
||||
}
|
||||
case *SetViewportScroll:
|
||||
return exs.HandleScroll(msg, messageID, timestamp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import (
|
|||
type inputLabels map[uint64]string
|
||||
|
||||
type inputEventBuilder struct {
|
||||
inputEvent *InputEvent
|
||||
inputLabels inputLabels
|
||||
inputID uint64
|
||||
inputEvent *InputEvent
|
||||
inputLabels inputLabels
|
||||
inputID uint64
|
||||
}
|
||||
|
||||
func NewInputEventBuilder() *inputEventBuilder {
|
||||
|
|
@ -18,6 +18,25 @@ func NewInputEventBuilder() *inputEventBuilder {
|
|||
return ieBuilder
|
||||
}
|
||||
|
||||
func (ib *inputEventBuilder) HandleMessage(message Message, messageID uint64, timestamp uint64) *InputEvent {
|
||||
switch msg := message.(type) {
|
||||
//case *SessionDisconnect:
|
||||
// i := ib.Build()
|
||||
// ib.ClearLabels()
|
||||
// return i
|
||||
case *SetPageLocation:
|
||||
if msg.NavigationStart != 0 {
|
||||
i := ib.Build()
|
||||
ib.ClearLabels()
|
||||
return i
|
||||
}
|
||||
case *SetInputTarget:
|
||||
return ib.HandleSetInputTarget(msg)
|
||||
case *SetInputValue:
|
||||
return ib.HandleSetInputValue(msg, messageID, timestamp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *inputEventBuilder) ClearLabels() {
|
||||
b.inputLabels = make(inputLabels)
|
||||
|
|
@ -57,7 +76,7 @@ func (b *inputEventBuilder) HasInstance() bool {
|
|||
return b.inputEvent != nil
|
||||
}
|
||||
|
||||
func (b * inputEventBuilder) GetTimestamp() uint64 {
|
||||
func (b *inputEventBuilder) GetTimestamp() uint64 {
|
||||
if b.inputEvent == nil {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -69,10 +88,10 @@ func (b *inputEventBuilder) Build() *InputEvent {
|
|||
return nil
|
||||
}
|
||||
inputEvent := b.inputEvent
|
||||
label, exists := b.inputLabels[b.inputID]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
label := b.inputLabels[b.inputID]
|
||||
// if !ok {
|
||||
// return nil
|
||||
// }
|
||||
inputEvent.Label = label
|
||||
|
||||
b.inputEvent = nil
|
||||
82
backend/services/detectors/builder/memoryIssueFinder.go
Normal file
82
backend/services/detectors/builder/memoryIssueFinder.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
const MIN_COUNT = 3
|
||||
const MEM_RATE_THRESHOLD = 300 // % to average
|
||||
|
||||
type memoryIssueDetector struct {
|
||||
startMessageID uint64
|
||||
startTimestamp uint64
|
||||
rate int
|
||||
count float64
|
||||
sum float64
|
||||
contextString string
|
||||
}
|
||||
|
||||
func (f *memoryIssueDetector) HandleMessage(message Message, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
switch msg := message.(type) {
|
||||
case *SetPageLocation:
|
||||
f.HandleSetPageLocation(msg)
|
||||
case *PerformanceTrack:
|
||||
return f.HandlePerformanceTrack(msg, messageID, timestamp)
|
||||
case *SessionEnd:
|
||||
return f.Build()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *memoryIssueDetector) Build() *IssueEvent {
|
||||
if f.startTimestamp == 0 {
|
||||
return nil
|
||||
}
|
||||
payload, _ := json.Marshal(struct{ Rate int }{f.rate - 100})
|
||||
i := &IssueEvent{
|
||||
Type: "memory",
|
||||
Timestamp: f.startTimestamp,
|
||||
MessageID: f.startMessageID,
|
||||
ContextString: f.contextString,
|
||||
Payload: string(payload),
|
||||
}
|
||||
f.startTimestamp = 0
|
||||
f.startMessageID = 0
|
||||
f.rate = 0
|
||||
return i
|
||||
}
|
||||
|
||||
func (f *memoryIssueDetector) HandleSetPageLocation(msg *SetPageLocation) {
|
||||
f.contextString = msg.URL
|
||||
}
|
||||
|
||||
func (f *memoryIssueDetector) HandlePerformanceTrack(msg *PerformanceTrack, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
if f.count < MIN_COUNT {
|
||||
f.sum += float64(msg.UsedJSHeapSize)
|
||||
f.count++
|
||||
return nil
|
||||
}
|
||||
|
||||
average := f.sum / f.count
|
||||
rate := int(math.Round(float64(msg.UsedJSHeapSize) / average * 100))
|
||||
|
||||
f.sum += float64(msg.UsedJSHeapSize)
|
||||
f.count++
|
||||
|
||||
if rate >= MEM_RATE_THRESHOLD {
|
||||
if f.startTimestamp == 0 {
|
||||
f.startTimestamp = timestamp
|
||||
f.startMessageID = messageID
|
||||
}
|
||||
if f.rate < rate {
|
||||
f.rate = rate
|
||||
}
|
||||
} else {
|
||||
return f.Build()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -5,8 +5,26 @@ import (
|
|||
)
|
||||
|
||||
type pageEventBuilder struct {
|
||||
pageEvent *PageEvent
|
||||
firstTimingHandled bool
|
||||
pageEvent *PageEvent
|
||||
firstTimingHandled bool
|
||||
}
|
||||
|
||||
func (pe *pageEventBuilder) HandleMessage(message Message, messageID uint64, timestamp uint64) *PageEvent {
|
||||
switch msg := message.(type) {
|
||||
case *SetPageLocation:
|
||||
if msg.NavigationStart != 0 {
|
||||
e := pe.Build()
|
||||
pe.HandleSetPageLocation(msg, messageID, timestamp)
|
||||
return e
|
||||
}
|
||||
case *PageLoadTiming:
|
||||
return pe.HandlePageLoadTiming(msg)
|
||||
case *PageRenderTiming:
|
||||
return pe.HandlePageRenderTiming(msg)
|
||||
//case *SessionDisconnect:
|
||||
// return pe.Build()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *pageEventBuilder) buildIfTimingsComplete() *PageEvent {
|
||||
|
|
@ -28,7 +46,7 @@ func (b *pageEventBuilder) HandleSetPageLocation(msg *SetPageLocation, messageID
|
|||
}
|
||||
}
|
||||
|
||||
func (b * pageEventBuilder) HandlePageLoadTiming(msg *PageLoadTiming) *PageEvent {
|
||||
func (b *pageEventBuilder) HandlePageLoadTiming(msg *PageLoadTiming) *PageEvent {
|
||||
if !b.HasInstance() {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -62,7 +80,7 @@ func (b * pageEventBuilder) HandlePageLoadTiming(msg *PageLoadTiming) *PageEvent
|
|||
return b.buildIfTimingsComplete()
|
||||
}
|
||||
|
||||
func (b * pageEventBuilder) HandlePageRenderTiming(msg *PageRenderTiming) *PageEvent {
|
||||
func (b *pageEventBuilder) HandlePageRenderTiming(msg *PageRenderTiming) *PageEvent {
|
||||
if !b.HasInstance() {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -76,16 +94,16 @@ func (b *pageEventBuilder) HasInstance() bool {
|
|||
return b.pageEvent != nil
|
||||
}
|
||||
|
||||
func (b * pageEventBuilder) GetTimestamp() uint64 {
|
||||
func (b *pageEventBuilder) GetTimestamp() uint64 {
|
||||
if b.pageEvent == nil {
|
||||
return 0
|
||||
}
|
||||
return b.pageEvent.Timestamp;
|
||||
return b.pageEvent.Timestamp
|
||||
}
|
||||
|
||||
func (b * pageEventBuilder) Build() *PageEvent {
|
||||
func (b *pageEventBuilder) Build() *PageEvent {
|
||||
pageEvent := b.pageEvent
|
||||
b.pageEvent = nil
|
||||
b.firstTimingHandled = false
|
||||
return pageEvent
|
||||
}
|
||||
}
|
||||
|
|
@ -3,21 +3,29 @@ package builder
|
|||
import (
|
||||
"math"
|
||||
|
||||
"openreplay/backend/pkg/messages/performance"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
"openreplay/backend/pkg/messages/performance"
|
||||
)
|
||||
|
||||
|
||||
type performanceTrackAggrBuilder struct {
|
||||
performanceTrackAggr *PerformanceTrackAggr
|
||||
lastTimestamp uint64
|
||||
count float64
|
||||
sumFrameRate float64
|
||||
sumTickRate float64
|
||||
sumTotalJSHeapSize float64
|
||||
sumUsedJSHeapSize float64
|
||||
performanceTrackAggr *PerformanceTrackAggr
|
||||
lastTimestamp uint64
|
||||
count float64
|
||||
sumFrameRate float64
|
||||
sumTickRate float64
|
||||
sumTotalJSHeapSize float64
|
||||
sumUsedJSHeapSize float64
|
||||
}
|
||||
|
||||
func (pta *performanceTrackAggrBuilder) HandleMessage(message Message, messageID uint64, timestamp uint64) *PerformanceTrackAggr {
|
||||
switch msg := message.(type) {
|
||||
//case *SessionDisconnect:
|
||||
// return pta.Build()
|
||||
case *PerformanceTrack:
|
||||
return pta.HandlePerformanceTrack(msg, timestamp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *performanceTrackAggrBuilder) start(timestamp uint64) {
|
||||
b.performanceTrackAggr = &PerformanceTrackAggr{
|
||||
|
|
@ -39,7 +47,7 @@ func (b *performanceTrackAggrBuilder) HandlePerformanceTrack(msg *PerformanceTra
|
|||
}
|
||||
|
||||
frameRate := performance.FrameRate(msg.Frames, dt)
|
||||
tickRate := performance.TickRate(msg.Ticks, dt)
|
||||
tickRate := performance.TickRate(msg.Ticks, dt)
|
||||
|
||||
fps := uint64(math.Round(frameRate))
|
||||
cpu := performance.CPURateFromTickRate(tickRate)
|
||||
|
|
@ -84,7 +92,7 @@ func (b *performanceTrackAggrBuilder) GetStartTimestamp() uint64 {
|
|||
if b.performanceTrackAggr == nil {
|
||||
return 0
|
||||
}
|
||||
return b.performanceTrackAggr.TimestampStart;
|
||||
return b.performanceTrackAggr.TimestampStart
|
||||
}
|
||||
|
||||
func (b *performanceTrackAggrBuilder) Build() *PerformanceTrackAggr {
|
||||
|
|
@ -106,4 +114,3 @@ func (b *performanceTrackAggrBuilder) Build() *PerformanceTrackAggr {
|
|||
b.lastTimestamp = 0
|
||||
return performanceTrackAggr
|
||||
}
|
||||
|
||||
48
backend/services/detectors/builder/quickReturnDetector.go
Normal file
48
backend/services/detectors/builder/quickReturnDetector.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
const QUICK_RETURN_THRESHOLD = 3 * 1000
|
||||
|
||||
type quickReturnDetector struct {
|
||||
timestamp uint64
|
||||
currentPage string
|
||||
basePage string
|
||||
}
|
||||
|
||||
func (qrd *quickReturnDetector) HandleMessage(message Message, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
switch msg := message.(type) {
|
||||
case *SetPageLocation:
|
||||
if msg.NavigationStart != 0 {
|
||||
return qrd.HandleSetPageLocation(msg, messageID, timestamp)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qrd *quickReturnDetector) HandleSetPageLocation(msg *SetPageLocation, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
|
||||
if (timestamp-qrd.timestamp > 0) && (timestamp-qrd.timestamp < QUICK_RETURN_THRESHOLD) && (
|
||||
msg.Referrer == qrd.basePage) {
|
||||
i := &IssueEvent{
|
||||
Type: "quick_return",
|
||||
Timestamp: timestamp,
|
||||
MessageID: messageID,
|
||||
ContextString: "Quick return from a page",
|
||||
Context: msg.Referrer + ", " + qrd.currentPage,
|
||||
Payload: ""}
|
||||
qrd.basePage = qrd.currentPage
|
||||
qrd.currentPage = msg.Referrer
|
||||
qrd.timestamp = timestamp
|
||||
return i
|
||||
}
|
||||
|
||||
if msg.Referrer != qrd.currentPage {
|
||||
qrd.timestamp = timestamp
|
||||
qrd.basePage = qrd.currentPage
|
||||
qrd.currentPage = msg.Referrer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
const MAX_HISTORY_OF_INPUTS = 10
|
||||
|
||||
type repetitiveInputDetector struct {
|
||||
lastInputs []string
|
||||
currentPage string
|
||||
}
|
||||
|
||||
func (rid *repetitiveInputDetector) HandleMessage(message Message, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
switch msg := message.(type) {
|
||||
case *InputEvent:
|
||||
return rid.HandleInputEvent(msg, messageID, timestamp)
|
||||
case *SetPageLocation:
|
||||
if msg.NavigationStart != 0 {
|
||||
rid.HandleSetPageLocation(msg)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rid *repetitiveInputDetector) HandleInputEvent(msg *InputEvent, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
// Check if the input is already in cache
|
||||
for i, value := range rid.lastInputs {
|
||||
if value == msg.Value {
|
||||
|
||||
// Update cache
|
||||
rid.lastInputs = append(rid.lastInputs[:i], rid.lastInputs[i+1:]...)
|
||||
rid.lastInputs = append(rid.lastInputs, msg.Value)
|
||||
|
||||
// Build an issue
|
||||
return &IssueEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: timestamp,
|
||||
Type: "repetitive_input",
|
||||
Payload: "",
|
||||
Context: rid.currentPage,
|
||||
ContextString: "The same input has recently been typed in"}
|
||||
}
|
||||
}
|
||||
|
||||
// Append a message value to cache
|
||||
rid.lastInputs = append(rid.lastInputs, msg.Value)
|
||||
|
||||
// Discard last element from the queue
|
||||
if len(rid.lastInputs) >= MAX_HISTORY_OF_INPUTS {
|
||||
rid.lastInputs = rid.lastInputs[:len(rid.lastInputs)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rid *repetitiveInputDetector) HandleSetPageLocation(msg *SetPageLocation) {
|
||||
rid.currentPage = msg.Referrer
|
||||
}
|
||||
61
backend/services/detectors/builder/resourceEventBuilder.go
Normal file
61
backend/services/detectors/builder/resourceEventBuilder.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
type resourceEventBuilder struct {}
|
||||
|
||||
func (reb *resourceEventBuilder) HandleMessage(message Message, messageID uint64) *ResourceEvent{
|
||||
switch msg := message.(type) {
|
||||
case *ResourceTiming:
|
||||
tp := getResourceType(msg.Initiator, msg.URL)
|
||||
success := msg.Duration != 0
|
||||
return &ResourceEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: msg.Timestamp,
|
||||
Duration: msg.Duration,
|
||||
TTFB: msg.TTFB,
|
||||
HeaderSize: msg.HeaderSize,
|
||||
EncodedBodySize: msg.EncodedBodySize,
|
||||
DecodedBodySize: msg.DecodedBodySize,
|
||||
URL: msg.URL,
|
||||
Type: tp,
|
||||
Success: success,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getURLExtention(URL string) string {
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
i := strings.LastIndex(u.Path, ".")
|
||||
return u.Path[i+1:]
|
||||
}
|
||||
|
||||
func getResourceType(initiator string, URL string) string {
|
||||
switch initiator {
|
||||
case "xmlhttprequest", "fetch":
|
||||
return "fetch"
|
||||
case "img":
|
||||
return "img"
|
||||
default:
|
||||
switch getURLExtention(URL) {
|
||||
case "css":
|
||||
return "stylesheet"
|
||||
case "js":
|
||||
return "script"
|
||||
case "png", "gif", "jpg", "jpeg", "svg":
|
||||
return "img"
|
||||
case "mp4", "mkv", "ogg", "webm", "avi", "mp3":
|
||||
return "media"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
}
|
||||
89
backend/services/detectors/builder/simpleEventsBuilder.go
Normal file
89
backend/services/detectors/builder/simpleEventsBuilder.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
func (b *builder) HandleSimpleMessages(message Message, messageID uint64) {
|
||||
switch msg := message.(type) {
|
||||
case *MouseClick:
|
||||
if msg.Label != "" {
|
||||
b.appendReadyMessage(&ClickEvent{
|
||||
MessageID: messageID,
|
||||
Label: msg.Label,
|
||||
HesitationTime: msg.HesitationTime,
|
||||
Timestamp: b.timestamp,
|
||||
})
|
||||
}
|
||||
case *RawErrorEvent:
|
||||
b.appendReadyMessage(&ErrorEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: msg.Timestamp,
|
||||
Source: msg.Source,
|
||||
Name: msg.Name,
|
||||
Message: msg.Message,
|
||||
Payload: msg.Payload,
|
||||
})
|
||||
case *JSException:
|
||||
b.appendReadyMessage(&ErrorEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: b.timestamp,
|
||||
Source: "js_exception",
|
||||
Name: msg.Name,
|
||||
Message: msg.Message,
|
||||
Payload: msg.Payload,
|
||||
})
|
||||
case *ResourceTiming:
|
||||
success := msg.Duration != 0
|
||||
tp := getResourceType(msg.Initiator, msg.URL)
|
||||
if !success && tp == "fetch" {
|
||||
b.appendReadyMessage(&IssueEvent{
|
||||
Type: "bad_request",
|
||||
MessageID: messageID,
|
||||
Timestamp: msg.Timestamp,
|
||||
ContextString: msg.URL,
|
||||
Context: "",
|
||||
Payload: "",
|
||||
})
|
||||
}
|
||||
case *RawCustomEvent:
|
||||
b.appendReadyMessage(&CustomEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: b.timestamp,
|
||||
Name: msg.Name,
|
||||
Payload: msg.Payload,
|
||||
})
|
||||
case *CustomIssue:
|
||||
b.appendReadyMessage(&IssueEvent{
|
||||
Type: "custom",
|
||||
Timestamp: b.timestamp,
|
||||
MessageID: messageID,
|
||||
ContextString: msg.Name,
|
||||
Payload: msg.Payload,
|
||||
})
|
||||
case *Fetch:
|
||||
b.appendReadyMessage(&ResourceEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: msg.Timestamp,
|
||||
Duration: msg.Duration,
|
||||
URL: msg.URL,
|
||||
Type: "fetch",
|
||||
Success: msg.Status < 300,
|
||||
Method: msg.Method,
|
||||
Status: msg.Status,
|
||||
})
|
||||
case *StateAction:
|
||||
b.appendReadyMessage(&StateActionEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: b.timestamp,
|
||||
Type: msg.Type,
|
||||
})
|
||||
case *GraphQL:
|
||||
b.appendReadyMessage(&GraphQLEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: b.timestamp,
|
||||
Name: msg.OperationName,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
72
backend/services/detectors/main.go
Normal file
72
backend/services/detectors/main.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"openreplay/backend/pkg/intervals"
|
||||
"openreplay/backend/pkg/env"
|
||||
"openreplay/backend/pkg/messages"
|
||||
"openreplay/backend/pkg/queue"
|
||||
"openreplay/backend/pkg/queue/types"
|
||||
"openreplay/backend/services/detectors/builder"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile)
|
||||
|
||||
GROUP_EVENTS := env.String("GROUP_DETECTOR") // env.String("GROUP_EVENTS")
|
||||
TOPIC_RAW := env.String("TOPIC_RAW")
|
||||
TOPIC_TRIGGER := env.String("TOPIC_TRIGGER")
|
||||
|
||||
builderMap := builder.NewBuilderMap()
|
||||
var lastTs int64 = 0
|
||||
|
||||
producer := queue.NewProducer()
|
||||
consumer := queue.NewMessageConsumer(
|
||||
GROUP_EVENTS,
|
||||
[]string{TOPIC_RAW},
|
||||
func(sessionID uint64, msg messages.Message, meta *types.Meta) {
|
||||
lastTs = meta.Timestamp
|
||||
builderMap.HandleMessage(sessionID, msg, msg.Meta().Index)
|
||||
builderMap.IterateSessionReadyMessages(sessionID, lastTs, func(readyMsg messages.Message) {
|
||||
producer.Produce(TOPIC_TRIGGER, sessionID, messages.Encode(readyMsg))
|
||||
})
|
||||
},
|
||||
)
|
||||
consumer.DisableAutoCommit()
|
||||
|
||||
tick := time.Tick(intervals.EVENTS_COMMIT_INTERVAL * time.Millisecond)
|
||||
|
||||
sigchan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
for {
|
||||
select {
|
||||
case sig := <-sigchan:
|
||||
log.Printf("Caught signal %v: terminating\n", sig)
|
||||
producer.Close(2000)
|
||||
consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP)
|
||||
consumer.Close()
|
||||
os.Exit(0)
|
||||
case <-tick:
|
||||
builderMap.IterateReadyMessages(time.Now().UnixNano()/1e6, func(sessionID uint64, readyMsg messages.Message) {
|
||||
if _, ok := readyMsg.(*messages.SessionEnd); ok {
|
||||
log.Printf("ENDSOME %v", sessionID)
|
||||
}
|
||||
producer.Produce(TOPIC_TRIGGER, sessionID, messages.Encode(readyMsg))
|
||||
})
|
||||
// TODO: why exactly do we need Flush here and not in any other place?
|
||||
producer.Flush(2000)
|
||||
consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP)
|
||||
default:
|
||||
if err := consumer.ConsumeNext(); err != nil {
|
||||
log.Printf("Error on consuming: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +1,21 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"log"
|
||||
|
||||
"openreplay/backend/pkg/intervals"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
func getURLExtention(URL string) string {
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
i := strings.LastIndex(u.Path, ".")
|
||||
return u.Path[i+1:]
|
||||
}
|
||||
|
||||
func getResourceType(initiator string, URL string) string {
|
||||
switch initiator {
|
||||
case "xmlhttprequest", "fetch":
|
||||
return "fetch"
|
||||
case "img":
|
||||
return "img"
|
||||
default:
|
||||
switch getURLExtention(URL) {
|
||||
case "css":
|
||||
return "stylesheet"
|
||||
case "js":
|
||||
return "script"
|
||||
case "png", "gif", "jpg", "jpeg", "svg":
|
||||
return "img"
|
||||
case "mp4", "mkv", "ogg", "webm", "avi", "mp3":
|
||||
return "media"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type builder struct {
|
||||
readyMsgs []Message
|
||||
timestamp uint64
|
||||
peBuilder *pageEventBuilder
|
||||
ptaBuilder *performanceTrackAggrBuilder
|
||||
ieBuilder *inputEventBuilder
|
||||
ciFinder *cpuIssueFinder
|
||||
miFinder *memoryIssueFinder
|
||||
ddDetector *domDropDetector
|
||||
crDetector *clickRageDetector
|
||||
dcDetector *deadClickDetector
|
||||
integrationsWaiting bool
|
||||
|
||||
|
||||
sid uint64
|
||||
readyMsgs []Message
|
||||
timestamp uint64
|
||||
integrationsWaiting bool
|
||||
sid uint64
|
||||
}
|
||||
|
||||
func NewBuilder() *builder {
|
||||
return &builder{
|
||||
peBuilder: &pageEventBuilder{},
|
||||
ptaBuilder: &performanceTrackAggrBuilder{},
|
||||
ieBuilder: NewInputEventBuilder(),
|
||||
ciFinder: &cpuIssueFinder{},
|
||||
miFinder: &memoryIssueFinder{},
|
||||
ddDetector: &domDropDetector{},
|
||||
crDetector: &clickRageDetector{},
|
||||
dcDetector: &deadClickDetector{},
|
||||
integrationsWaiting: true,
|
||||
}
|
||||
}
|
||||
|
|
@ -82,234 +32,35 @@ func (b *builder) iterateReadyMessage(iter func(msg Message)) {
|
|||
}
|
||||
|
||||
func (b *builder) buildSessionEnd() {
|
||||
if b.timestamp == 0 {
|
||||
return
|
||||
}
|
||||
sessionEnd := &SessionEnd{
|
||||
Timestamp: b.timestamp, // + delay?
|
||||
}
|
||||
b.appendReadyMessage(sessionEnd)
|
||||
}
|
||||
|
||||
func (b *builder) buildPageEvent() {
|
||||
if msg := b.peBuilder.Build(); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
func (b *builder) buildPerformanceTrackAggr() {
|
||||
if msg := b.ptaBuilder.Build(); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
func (b *builder) buildInputEvent() {
|
||||
if msg := b.ieBuilder.Build(); msg != nil {
|
||||
b.appendReadyMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *builder) handleMessage(message Message, messageID uint64) {
|
||||
timestamp := uint64(message.Meta().Timestamp)
|
||||
if b.timestamp <= timestamp { // unnecessary. TODO: test and remove
|
||||
b.timestamp = timestamp
|
||||
}
|
||||
// Before the first timestamp.
|
||||
switch msg := message.(type) {
|
||||
case *SessionStart,
|
||||
*Metadata,
|
||||
*UserID,
|
||||
*UserAnonymousID:
|
||||
b.appendReadyMessage(msg)
|
||||
case *RawErrorEvent:
|
||||
b.appendReadyMessage(&ErrorEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: msg.Timestamp,
|
||||
Source: msg.Source,
|
||||
Name: msg.Name,
|
||||
Message: msg.Message,
|
||||
Payload: msg.Payload,
|
||||
})
|
||||
case *SessionDisconnect:
|
||||
b.timestamp = msg.Timestamp
|
||||
case *SessionStart:
|
||||
b.timestamp = msg.Timestamp
|
||||
case *Timestamp:
|
||||
b.timestamp = msg.Timestamp
|
||||
}
|
||||
// Start from the first timestamp event.
|
||||
if b.timestamp == 0 {
|
||||
return
|
||||
}
|
||||
switch msg := message.(type) {
|
||||
case *SetPageLocation:
|
||||
if msg.NavigationStart == 0 {
|
||||
b.appendReadyMessage(&PageEvent{
|
||||
URL: msg.URL,
|
||||
Referrer: msg.Referrer,
|
||||
Loaded: false,
|
||||
MessageID: messageID,
|
||||
Timestamp: b.timestamp,
|
||||
})
|
||||
} else {
|
||||
b.buildPageEvent()
|
||||
b.buildInputEvent()
|
||||
b.ieBuilder.ClearLabels()
|
||||
b.peBuilder.HandleSetPageLocation(msg, messageID, b.timestamp)
|
||||
b.miFinder.HandleSetPageLocation(msg)
|
||||
b.ciFinder.HandleSetPageLocation(msg)
|
||||
}
|
||||
case *PageLoadTiming:
|
||||
if rm := b.peBuilder.HandlePageLoadTiming(msg); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
case *PageRenderTiming:
|
||||
if rm := b.peBuilder.HandlePageRenderTiming(msg); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
case *PerformanceTrack:
|
||||
if rm := b.ptaBuilder.HandlePerformanceTrack(msg, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
if rm := b.ciFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
if rm := b.miFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
case *SetInputTarget:
|
||||
if rm := b.ieBuilder.HandleSetInputTarget(msg); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
case *SetInputValue:
|
||||
if rm := b.ieBuilder.HandleSetInputValue(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
case *MouseClick:
|
||||
b.buildInputEvent()
|
||||
if rm := b.crDetector.HandleMouseClick(msg, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
if msg.Label != "" {
|
||||
b.appendReadyMessage(&ClickEvent{
|
||||
MessageID: messageID,
|
||||
Label: msg.Label,
|
||||
HesitationTime: msg.HesitationTime,
|
||||
Timestamp: b.timestamp,
|
||||
})
|
||||
}
|
||||
case *JSException:
|
||||
b.appendReadyMessage(&ErrorEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: b.timestamp,
|
||||
Source: "js_exception",
|
||||
Name: msg.Name,
|
||||
Message: msg.Message,
|
||||
Payload: msg.Payload,
|
||||
})
|
||||
case *ResourceTiming:
|
||||
tp := getResourceType(msg.Initiator, msg.URL)
|
||||
success := msg.Duration != 0
|
||||
b.appendReadyMessage(&ResourceEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: msg.Timestamp,
|
||||
Duration: msg.Duration,
|
||||
TTFB: msg.TTFB,
|
||||
HeaderSize: msg.HeaderSize,
|
||||
EncodedBodySize: msg.EncodedBodySize,
|
||||
DecodedBodySize: msg.DecodedBodySize,
|
||||
URL: msg.URL,
|
||||
Type: tp,
|
||||
Success: success,
|
||||
})
|
||||
if !success && tp == "fetch" {
|
||||
b.appendReadyMessage(&IssueEvent{
|
||||
Type: "bad_request",
|
||||
MessageID: messageID,
|
||||
Timestamp: msg.Timestamp,
|
||||
ContextString: msg.URL,
|
||||
Context: "",
|
||||
Payload: "",
|
||||
})
|
||||
}
|
||||
case *RawCustomEvent:
|
||||
b.appendReadyMessage(&CustomEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: b.timestamp,
|
||||
Name: msg.Name,
|
||||
Payload: msg.Payload,
|
||||
})
|
||||
case *CustomIssue:
|
||||
b.appendReadyMessage(&IssueEvent{
|
||||
Type: "custom",
|
||||
Timestamp: b.timestamp,
|
||||
MessageID: messageID,
|
||||
ContextString: msg.Name,
|
||||
Payload: msg.Payload,
|
||||
})
|
||||
case *Fetch:
|
||||
b.appendReadyMessage(&ResourceEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: msg.Timestamp,
|
||||
Duration: msg.Duration,
|
||||
URL: msg.URL,
|
||||
Type: "fetch",
|
||||
Success: msg.Status < 300,
|
||||
Method: msg.Method,
|
||||
Status: msg.Status,
|
||||
})
|
||||
case *StateAction:
|
||||
b.appendReadyMessage(&StateActionEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: b.timestamp,
|
||||
Type: msg.Type,
|
||||
})
|
||||
case *GraphQL:
|
||||
b.appendReadyMessage(&GraphQLEvent{
|
||||
MessageID: messageID,
|
||||
Timestamp: b.timestamp,
|
||||
Name: msg.OperationName,
|
||||
})
|
||||
case *CreateElementNode,
|
||||
*CreateTextNode:
|
||||
b.ddDetector.HandleNodeCreation()
|
||||
case *RemoveNode:
|
||||
b.ddDetector.HandleNodeRemoval(b.timestamp)
|
||||
case *CreateDocument:
|
||||
if rm := b.ddDetector.Build(); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
}
|
||||
if rm := b.dcDetector.HandleMessage(message, messageID, b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (b *builder) checkTimeouts(ts int64) bool {
|
||||
if b.timestamp == 0 {
|
||||
if b.timestamp == 0 {
|
||||
return false // There was no timestamp events yet
|
||||
}
|
||||
|
||||
if b.peBuilder.HasInstance() && int64(b.peBuilder.GetTimestamp())+intervals.EVENTS_PAGE_EVENT_TIMEOUT < ts {
|
||||
b.buildPageEvent()
|
||||
}
|
||||
if b.ieBuilder.HasInstance() && int64(b.ieBuilder.GetTimestamp())+intervals.EVENTS_INPUT_EVENT_TIMEOUT < ts {
|
||||
b.buildInputEvent()
|
||||
}
|
||||
if b.ptaBuilder.HasInstance() && int64(b.ptaBuilder.GetStartTimestamp())+intervals.EVENTS_PERFORMANCE_AGGREGATION_TIMEOUT < ts {
|
||||
b.buildPerformanceTrackAggr()
|
||||
}
|
||||
|
||||
lastTsGap := ts - int64(b.timestamp)
|
||||
//log.Printf("checking timeouts for sess %v: %v now, %v sesstime; gap %v",b.sid, ts, b.timestamp, lastTsGap)
|
||||
log.Printf("checking timeouts for sess %v: %v now, %v sesstime; gap %v", b.sid, ts, b.timestamp, lastTsGap)
|
||||
if lastTsGap > intervals.EVENTS_SESSION_END_TIMEOUT {
|
||||
if rm := b.ddDetector.Build(); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
if rm := b.ciFinder.Build(); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
if rm := b.miFinder.Build(); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
if rm := b.crDetector.Build(); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
if rm := b.dcDetector.HandleReaction(b.timestamp); rm != nil {
|
||||
b.appendReadyMessage(rm)
|
||||
}
|
||||
b.buildSessionEnd()
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ func NewBuilderMap() builderMap {
|
|||
return make(builderMap)
|
||||
}
|
||||
|
||||
func (m builderMap) GetBuilder(sessionID uint64) *builder {
|
||||
func (m builderMap) getBuilder(sessionID uint64) *builder {
|
||||
b := m[sessionID]
|
||||
if b == nil {
|
||||
b = NewBuilder()
|
||||
|
|
@ -23,7 +23,7 @@ func (m builderMap) GetBuilder(sessionID uint64) *builder {
|
|||
}
|
||||
|
||||
func (m builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint64) {
|
||||
b := m.GetBuilder(sessionID)
|
||||
b := m.getBuilder(sessionID)
|
||||
b.handleMessage(msg, messageID)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
|
||||
type domDropDetector struct {
|
||||
removedCount int
|
||||
lastDropTimestamp uint64
|
||||
}
|
||||
|
||||
const DROP_WINDOW = 200 //ms
|
||||
const CRITICAL_COUNT = 1 // Our login page contains 20. But on crush it removes only roots (1-3 nodes).
|
||||
|
||||
func (dd *domDropDetector) HandleNodeCreation() {
|
||||
dd.removedCount = 0
|
||||
dd.lastDropTimestamp = 0
|
||||
}
|
||||
|
||||
func (dd *domDropDetector) HandleNodeRemoval(ts uint64) {
|
||||
if dd.lastDropTimestamp + DROP_WINDOW > ts {
|
||||
dd.removedCount += 1
|
||||
} else {
|
||||
dd.removedCount = 1
|
||||
}
|
||||
dd.lastDropTimestamp = ts
|
||||
}
|
||||
|
||||
|
||||
func (dd *domDropDetector) Build() *DOMDrop {
|
||||
var domDrop *DOMDrop
|
||||
if dd.removedCount >= CRITICAL_COUNT {
|
||||
domDrop = &DOMDrop{
|
||||
Timestamp: dd.lastDropTimestamp,
|
||||
}
|
||||
}
|
||||
dd.removedCount = 0
|
||||
dd.lastDropTimestamp = 0
|
||||
return domDrop
|
||||
}
|
||||
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"math"
|
||||
"encoding/json"
|
||||
|
||||
. "openreplay/backend/pkg/messages"
|
||||
)
|
||||
|
||||
const MIN_COUNT = 3
|
||||
const MEM_RATE_THRESHOLD = 300 // % to average
|
||||
|
||||
type memoryIssueFinder struct {
|
||||
startMessageID uint64
|
||||
startTimestamp uint64
|
||||
rate int
|
||||
count float64
|
||||
sum float64
|
||||
contextString string
|
||||
}
|
||||
|
||||
func (f *memoryIssueFinder) Build() *IssueEvent {
|
||||
if f.startTimestamp == 0 {
|
||||
return nil
|
||||
}
|
||||
payload, _ := json.Marshal(struct{Rate int }{f.rate - 100,})
|
||||
i := &IssueEvent{
|
||||
Type: "memory",
|
||||
Timestamp: f.startTimestamp,
|
||||
MessageID: f.startMessageID,
|
||||
ContextString: f.contextString,
|
||||
Payload: string(payload),
|
||||
}
|
||||
f.startTimestamp = 0
|
||||
f.startMessageID = 0
|
||||
f.rate = 0
|
||||
return i
|
||||
}
|
||||
|
||||
func (f *memoryIssueFinder) HandleSetPageLocation(msg *SetPageLocation) {
|
||||
f.contextString = msg.URL
|
||||
}
|
||||
|
||||
func (f *memoryIssueFinder) HandlePerformanceTrack(msg *PerformanceTrack, messageID uint64, timestamp uint64) *IssueEvent {
|
||||
if f.count < MIN_COUNT {
|
||||
f.sum += float64(msg.UsedJSHeapSize)
|
||||
f.count++
|
||||
return nil
|
||||
}
|
||||
|
||||
average := f.sum/f.count
|
||||
rate := int(math.Round(float64(msg.UsedJSHeapSize)/average * 100))
|
||||
|
||||
f.sum += float64(msg.UsedJSHeapSize)
|
||||
f.count++
|
||||
|
||||
if rate >= MEM_RATE_THRESHOLD {
|
||||
if f.startTimestamp == 0 {
|
||||
f.startTimestamp = timestamp
|
||||
f.startMessageID = messageID
|
||||
}
|
||||
if f.rate < rate {
|
||||
f.rate = rate
|
||||
}
|
||||
} else {
|
||||
return f.Build()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -20,7 +20,8 @@ import (
|
|||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile)
|
||||
|
||||
GROUP_EVENTS := env.String("GROUP_ENDER")
|
||||
GROUP_EVENTS := env.String("GROUP_ENDER") // env.String("GROUP_EVENTS")
|
||||
TOPIC_RAW := env.String("TOPIC_RAW")
|
||||
TOPIC_TRIGGER := env.String("TOPIC_TRIGGER")
|
||||
|
||||
builderMap := builder.NewBuilderMap()
|
||||
|
|
@ -29,15 +30,13 @@ func main() {
|
|||
producer := queue.NewProducer()
|
||||
consumer := queue.NewMessageConsumer(
|
||||
GROUP_EVENTS,
|
||||
[]string{
|
||||
env.String("TOPIC_RAW"),
|
||||
},
|
||||
[]string{ TOPIC_RAW },
|
||||
func(sessionID uint64, msg messages.Message, meta *types.Meta) {
|
||||
lastTs = meta.Timestamp
|
||||
builderMap.HandleMessage(sessionID, msg, msg.Meta().Index)
|
||||
// builderMap.IterateSessionReadyMessages(sessionID, lastTs, func(readyMsg messages.Message) {
|
||||
// producer.Produce(TOPIC_TRIGGER, sessionID, messages.Encode(readyMsg))
|
||||
// })
|
||||
builderMap.HandleMessage(sessionID, msg, meta.ID)
|
||||
builderMap.IterateSessionReadyMessages(sessionID, lastTs, func(readyMsg messages.Message) {
|
||||
producer.Produce(TOPIC_TRIGGER, sessionID, messages.Encode(readyMsg))
|
||||
})
|
||||
},
|
||||
)
|
||||
consumer.DisableAutoCommit()
|
||||
|
|
@ -57,6 +56,9 @@ func main() {
|
|||
os.Exit(0)
|
||||
case <- tick:
|
||||
builderMap.IterateReadyMessages(time.Now().UnixNano()/1e6, func(sessionID uint64, readyMsg messages.Message) {
|
||||
if _, ok := readyMsg.(*messages.SessionEnd); ok {
|
||||
log.Printf("ENDSOME %v", sessionID)
|
||||
}
|
||||
producer.Produce(TOPIC_TRIGGER, sessionID, messages.Encode(readyMsg))
|
||||
})
|
||||
// TODO: why exactly do we need Flush here and not in any other place?
|
||||
|
|
@ -64,7 +66,7 @@ func main() {
|
|||
consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP)
|
||||
default:
|
||||
if err := consumer.ConsumeNext(); err != nil {
|
||||
log.Fatalf("Error on consuming: %v", err)
|
||||
log.Printf("Error on consuming: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,11 @@ func (a *Alert) CanCheck() bool {
|
|||
}
|
||||
|
||||
func (a *Alert) Build() (sq.SelectBuilder, error) {
|
||||
colDef := LeftToDb[a.Query.Left]
|
||||
colDef, ok := LeftToDb[a.Query.Left]
|
||||
if !ok {
|
||||
return sq.Select(), errors.New(fmt.Sprintf("!! unsupported metric '%s' from alert: %d:%s\n", a.Query.Left, a.AlertID, a.Name))
|
||||
}
|
||||
|
||||
subQ := sq.
|
||||
Select(colDef.formula + " AS value").
|
||||
From(colDef.table).
|
||||
|
|
@ -221,4 +225,4 @@ func (a *Alert) Build() (sq.SelectBuilder, error) {
|
|||
return q, errors.New("unsupported detection method")
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -16,13 +16,12 @@ We hope your cluster has provision to create a [service type](https://kubernetes
|
|||
cd helm && bash kube-install.sh
|
||||
```
|
||||
|
||||
### Management of OpenReplay apps
|
||||
### OpenReplay CLI
|
||||
|
||||
- **openreplay-cli:**
|
||||
The CLI is helpful for managing basic aspects of your OpenReplay instance, things such as restarting or reinstalling a service, accessing a component's logs or simply checking the status of your backend services. Below the list of covered operations:
|
||||
|
||||
This script will help to manage OpenReplay applications. Basic operations covered are
|
||||
- status: status of the applications
|
||||
- logs: logs of a specific application
|
||||
- status: status of the running services
|
||||
- logs: logs of a specific service
|
||||
- stop: stop one or all services
|
||||
- start: start one or all services
|
||||
- restart: restart one or all services
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ set -o errtrace
|
|||
domain_name=`grep domain_name vars.yaml | grep -v "example" | cut -d " " -f2 | cut -d '"' -f2`
|
||||
# Ref: https://stackoverflow.com/questions/15268987/bash-based-regex-domain-name-validation
|
||||
[[ $(echo $domain_name | grep -P '(?=^.{5,254}$)(^(?:(?!\d+\.)[a-zA-Z0-9_\-]{1,63}\.?)+(?:[a-zA-Z]{2,})$)') ]] || {
|
||||
echo "OpenReplay Needs a valid domain name for captured sessions to replay. For example, openreplay.mycompany.com"
|
||||
echo "Please enter your domain name"
|
||||
echo "OpenReplay requires a valid domain name to properly work (i.e. openreplay.mycompany.com)"
|
||||
echo "Please enter your domain name:"
|
||||
read domain_name
|
||||
[[ -z domain_name ]] && {
|
||||
echo "OpenReplay won't work without domain name. Exiting..."
|
||||
|
|
@ -17,6 +17,22 @@ domain_name=`grep domain_name vars.yaml | grep -v "example" | cut -d " " -f2 | c
|
|||
}
|
||||
}
|
||||
|
||||
[[ $1 == "ee" ]] && {
|
||||
[[ $2 == "" ]] || {
|
||||
sed -i "s#enterprise_edition_license.*#enterprise_edition_license: ${2}#g" vars.yaml
|
||||
} && {
|
||||
echo """Enerprise edition license key is missing.
|
||||
Usage: ./install.sh ee XXXXXXXXXXXXX
|
||||
"""
|
||||
exit 1
|
||||
}
|
||||
# Resetting command line arguments.
|
||||
shift 2
|
||||
}
|
||||
|
||||
# https://parrot.asayer.io/os/license
|
||||
# payload: {"mid": "UUID of the machine", "license": ""}
|
||||
# response {"data":{"valid": TRUE|FALSE, "expiration": expiration date in ms}}
|
||||
|
||||
# Installing k3s
|
||||
curl -sL https://get.k3s.io | sudo K3S_KUBECONFIG_MODE="644" INSTALL_K3S_VERSION='v1.19.5+k3s2' INSTALL_K3S_EXEC="--no-deploy=traefik" sh -
|
||||
|
|
|
|||
|
|
@ -6,4 +6,3 @@ db_list:
|
|||
- "nfs-server-provisioner"
|
||||
- "postgresql"
|
||||
- "redis"
|
||||
enterprise_edition: false
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
delay: 60
|
||||
register: result
|
||||
until: result.rc == 0
|
||||
when: enterprise_edition
|
||||
when: enterprise_edition_license|length > 0
|
||||
- name: Restoring postgres data
|
||||
shell: |
|
||||
file="{{ item|basename }}"
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
delay: 60
|
||||
register: result
|
||||
until: result.rc == 0
|
||||
when: enterprise_edition
|
||||
when: enterprise_edition_license|length > 0
|
||||
- name: Initializing Minio
|
||||
shell: |
|
||||
minio_pod=$(kubectl get po -n db -l app.kubernetes.io/name=minio -n db --output custom-columns=name:.metadata.name | tail -n+2)
|
||||
|
|
|
|||
|
|
@ -108,3 +108,23 @@
|
|||
- pre-check
|
||||
- nginx
|
||||
- all
|
||||
- name: Checking Enterprise Licence
|
||||
uri:
|
||||
url: https://parrot.asayer.io/os/license
|
||||
body:
|
||||
mid: "UUID of the machine"
|
||||
license: "{{ enterprise_edition_license }}"
|
||||
body_format: json
|
||||
creates: "{{ ansible_env.HOME }}/.config/openreplay.license"
|
||||
follow_redirects: yes
|
||||
force: false
|
||||
http_agent: ansible-httpget
|
||||
method: POST
|
||||
return_content: true
|
||||
status_code: [200]
|
||||
timeout: 30
|
||||
unsafe_writes: false
|
||||
validate_certs: true
|
||||
when: enterprise_edition_license|length > 0
|
||||
register: enterprise_edition_license_check
|
||||
failed_when: enterprise_edition_license_check.json.data.valid != true
|
||||
|
|
|
|||
|
|
@ -57,3 +57,7 @@ enable_monitoring: "false"
|
|||
# `openssl rand -base64 30`
|
||||
minio_access_key: ""
|
||||
minio_secret_key: ""
|
||||
|
||||
# If you're using enterprise edition.
|
||||
# Insert the enterprise_edition_License key which you got.
|
||||
enterprise_edition_license: ""
|
||||
|
|
|
|||
6
tracker/tracker-axios/.gitignore
vendored
Normal file
6
tracker/tracker-axios/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
lib
|
||||
cjs
|
||||
.cache
|
||||
*.DS_Store
|
||||
5
tracker/tracker-axios/.npmignore
Normal file
5
tracker/tracker-axios/.npmignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
src
|
||||
tsconfig-cjs.json
|
||||
tsconfig.json
|
||||
.prettierrc.json
|
||||
.cache
|
||||
19
tracker/tracker-axios/LICENSE
Normal file
19
tracker/tracker-axios/LICENSE
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2021 OpenReplay.com <support@openreplay.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
42
tracker/tracker-axios/README.md
Normal file
42
tracker/tracker-axios/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# OpenReplay Tracker Axios plugin
|
||||
|
||||
Tracker plugin to support tracking of the [Axios](https://axios-http.com/) requests.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm i @openreplay/tracker-axios
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Initialize the `@openreplay/tracker` package as usual and load the plugin into it.
|
||||
|
||||
```js
|
||||
import Tracker from '@openreplay/tracker';
|
||||
import trackerAxios from '@openreplay/tracker-axios';
|
||||
|
||||
const tracker = new Tracker({
|
||||
projectKey: YOUR_PROJECT_KEY,
|
||||
});
|
||||
tracker.start();
|
||||
|
||||
tracker.use(trackerAxios());
|
||||
```
|
||||
Options:
|
||||
|
||||
```ts
|
||||
{
|
||||
instance: AxiosInstance; // default: axios
|
||||
failuresOnly: boolean; // default: true
|
||||
captureWhen: (AxiosRequestConfig) => boolean; // default: () => true
|
||||
sessionTokenHeader: string; // default: undefined
|
||||
}
|
||||
```
|
||||
By default plugin connects to the static `axios` instance, but you can specify one with the `instance` option.
|
||||
|
||||
Set `failuresOnly` option to `false` if you want to record every single request regardless of the status code. By default only failed requests are captured, when the axios' promise is rejected. You can also [regulate](https://github.com/axios/axios#request-config) this axios behaviour with the `validateStatus` option.
|
||||
|
||||
`captureWhen` parameter allows you to set a filter on what should be captured. The function will be called with the axios config object and expected to return `true` or `false`.
|
||||
|
||||
In case you use [OpenReplay integrations (sentry, bugsnag or others)](https://docs.openreplay.com/integrations), you can use `sessionTokenHeader` option to specify the header name. This header will be appended automatically to the each axios request and will contain OpenReplay session identificator value.
|
||||
823
tracker/tracker-axios/package-lock.json
generated
Normal file
823
tracker/tracker-axios/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,823 @@
|
|||
{
|
||||
"name": "@openreplay/tracker-axios",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
|
||||
"integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.12.13"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
|
||||
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
|
||||
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz",
|
||||
"integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.stat": "2.0.4",
|
||||
"run-parallel": "^1.1.9"
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.stat": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz",
|
||||
"integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@nodelib/fs.walk": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz",
|
||||
"integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.scandir": "2.1.4",
|
||||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@openreplay/tracker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@openreplay/tracker/-/tracker-3.0.3.tgz",
|
||||
"integrity": "sha512-50C2cwJFENeHNjXVV90uIA5YE1bxfGbhI8e76Nfw9Pg+GVN38DcvGhr3PJ3OKjioT9V4gXBbvtE/RDGRaJJWLA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"error-stack-parser": "^2.0.6"
|
||||
}
|
||||
},
|
||||
"@types/minimist": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz",
|
||||
"integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
|
||||
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true
|
||||
},
|
||||
"arrify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
||||
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true
|
||||
},
|
||||
"camelcase-keys": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
|
||||
"integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"camelcase": "^5.3.1",
|
||||
"map-obj": "^4.0.0",
|
||||
"quick-lru": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
},
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
|
||||
"dev": true
|
||||
},
|
||||
"decamelize-keys": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz",
|
||||
"integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"decamelize": "^1.1.0",
|
||||
"map-obj": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"map-obj": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
|
||||
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
|
||||
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-type": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"error-stack-parser": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz",
|
||||
"integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"stackframe": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz",
|
||||
"integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
"glob-parent": "^5.1.0",
|
||||
"merge2": "^1.3.0",
|
||||
"micromatch": "^4.0.2",
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"fastq": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz",
|
||||
"integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz",
|
||||
"integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==",
|
||||
"dev": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz",
|
||||
"integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-union": "^2.1.0",
|
||||
"dir-glob": "^3.0.1",
|
||||
"fast-glob": "^3.1.1",
|
||||
"ignore": "^5.1.4",
|
||||
"merge2": "^1.3.0",
|
||||
"slash": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"hard-rejection": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
|
||||
"integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
|
||||
"dev": true
|
||||
},
|
||||
"has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
|
||||
"dev": true
|
||||
},
|
||||
"imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
|
||||
"dev": true
|
||||
},
|
||||
"indent-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
|
||||
"dev": true
|
||||
},
|
||||
"is-core-module": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
|
||||
"integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
||||
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"is-plain-obj": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
|
||||
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
|
||||
"dev": true
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
|
||||
"dev": true
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"dev": true
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"dev": true
|
||||
},
|
||||
"lines-and-columns": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"dev": true
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"map-obj": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz",
|
||||
"integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"meow": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz",
|
||||
"integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/minimist": "^1.2.0",
|
||||
"camelcase-keys": "^6.2.2",
|
||||
"decamelize-keys": "^1.1.0",
|
||||
"hard-rejection": "^2.1.0",
|
||||
"minimist-options": "4.1.0",
|
||||
"normalize-package-data": "^2.5.0",
|
||||
"read-pkg-up": "^7.0.1",
|
||||
"redent": "^3.0.0",
|
||||
"trim-newlines": "^3.0.0",
|
||||
"type-fest": "^0.13.1",
|
||||
"yargs-parser": "^18.1.3"
|
||||
}
|
||||
},
|
||||
"merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.2.3"
|
||||
}
|
||||
},
|
||||
"min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist-options": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
|
||||
"integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arrify": "^1.0.1",
|
||||
"is-plain-obj": "^1.1.0",
|
||||
"kind-of": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"arrify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
|
||||
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hosted-git-info": "^2.1.4",
|
||||
"resolve": "^1.10.0",
|
||||
"semver": "2 || 3 || 4 || 5",
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"error-ex": "^1.3.1",
|
||||
"json-parse-even-better-errors": "^2.3.0",
|
||||
"lines-and-columns": "^1.1.6"
|
||||
}
|
||||
},
|
||||
"path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
|
||||
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
|
||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
||||
"dev": true
|
||||
},
|
||||
"queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true
|
||||
},
|
||||
"quick-lru": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
|
||||
"integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
|
||||
"dev": true
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
||||
"integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/normalize-package-data": "^2.4.0",
|
||||
"normalize-package-data": "^2.5.0",
|
||||
"parse-json": "^5.0.0",
|
||||
"type-fest": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"type-fest": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
|
||||
"integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-pkg-up": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
|
||||
"integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"find-up": "^4.1.0",
|
||||
"read-pkg": "^5.2.0",
|
||||
"type-fest": "^0.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"type-fest": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
|
||||
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"indent-string": "^4.0.0",
|
||||
"strip-indent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"replace-in-files-cli": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/replace-in-files-cli/-/replace-in-files-cli-1.0.0.tgz",
|
||||
"integrity": "sha512-/HMPLZeCA24CBUQ59ymHji6LyMKM+gEgDZlYsiPvXW6+3PdfOw6SsMCVd9KC2B+KlAEe/8vkJA6gfnexVdF15A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arrify": "^2.0.1",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"globby": "^11.0.1",
|
||||
"meow": "^7.1.1",
|
||||
"normalize-path": "^3.0.0",
|
||||
"write-file-atomic": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
|
||||
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-core-module": "^2.2.0",
|
||||
"path-parse": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"reusify": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true
|
||||
},
|
||||
"run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
|
||||
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
|
||||
"dev": true
|
||||
},
|
||||
"slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"spdx-correct": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
|
||||
"integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"spdx-exceptions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
|
||||
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
|
||||
"dev": true
|
||||
},
|
||||
"spdx-expression-parse": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
||||
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"spdx-exceptions": "^2.1.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"spdx-license-ids": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz",
|
||||
"integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==",
|
||||
"dev": true
|
||||
},
|
||||
"stackframe": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
|
||||
"integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==",
|
||||
"dev": true
|
||||
},
|
||||
"strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"min-indent": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"trim-newlines": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz",
|
||||
"integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==",
|
||||
"dev": true
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"dev": true
|
||||
},
|
||||
"typedarray-to-buffer": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-typedarray": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz",
|
||||
"integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==",
|
||||
"dev": true
|
||||
},
|
||||
"validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"spdx-correct": "^3.0.0",
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"write-file-atomic": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
|
||||
"integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-typedarray": "^1.0.0",
|
||||
"signal-exit": "^3.0.2",
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
tracker/tracker-axios/package.json
Normal file
33
tracker/tracker-axios/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "@openreplay/tracker-axios",
|
||||
"description": "Tracker plugin for axios requests recording",
|
||||
"version": "3.0.1",
|
||||
"axios": [
|
||||
"axios",
|
||||
"logging",
|
||||
"replay"
|
||||
],
|
||||
"author": "Aleksandr K <alex@openreplay.com>",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "./lib/index.js",
|
||||
"scripts": {
|
||||
"lint": "prettier --write 'src/**/*.ts' README.md && tsc --noEmit",
|
||||
"build": "npm run build-es && npm run build-cjs",
|
||||
"build-es": "rm -Rf lib && tsc",
|
||||
"build-cjs": "rm -Rf cjs && tsc --project tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > cjs/package.json && replace-in-files cjs/* --string='@openreplay/tracker' --replacement='@openreplay/tracker/cjs' && replace-in-files cjs/* --string='/lib/' --replacement='/'",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@openreplay/tracker": "^3.0.0",
|
||||
"axios": "^0.21.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openreplay/tracker": "^3.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"prettier": "^1.18.2",
|
||||
"replace-in-files-cli": "^1.0.0",
|
||||
"typescript": "^3.6.4"
|
||||
}
|
||||
}
|
||||
119
tracker/tracker-axios/src/index.ts
Normal file
119
tracker/tracker-axios/src/index.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { App, Messages } from '@openreplay/tracker';
|
||||
import { getExceptionMessage } from '@openreplay/tracker/lib/modules/exception'; // TODO: export from tracker root
|
||||
import { buildFullPath } from './url';
|
||||
|
||||
export interface Options {
|
||||
sessionTokenHeader?: string;
|
||||
instance: AxiosInstance;
|
||||
failuresOnly: boolean;
|
||||
captureWhen: (AxiosRequestConfig) => boolean;
|
||||
//ingoreHeaders: Array<string> | boolean;
|
||||
}
|
||||
|
||||
export default function(opts: Partial<Options> = {}) {
|
||||
const options: Options = Object.assign(
|
||||
{
|
||||
instance: axios,
|
||||
failuresOnly: true,
|
||||
captureWhen: () => true,
|
||||
//ingoreHeaders: [ 'Cookie', 'Set-Cookie', 'Authorization' ],
|
||||
},
|
||||
opts,
|
||||
);
|
||||
return (app: App | null) => {
|
||||
if (app === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendFetchMessage = (response: AxiosResponse) => {
|
||||
// @ts-ignore
|
||||
const startTime: number = response.config.__openreplayStartTs;
|
||||
const duration = performance.now() - startTime;
|
||||
if (typeof startTime !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
let requestData: string = '';
|
||||
if (typeof response.config.data === 'string') {
|
||||
requestData = response.config.data;
|
||||
} else {
|
||||
try {
|
||||
requestData = JSON.stringify(response.config.data) || '';
|
||||
} catch (e) {}
|
||||
}
|
||||
let responseData: string = '';
|
||||
if (typeof response.data === 'string') {
|
||||
responseData = response.data;
|
||||
} else {
|
||||
try {
|
||||
responseData = JSON.stringify(response.data) || '';
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Why can't axios propogate the final request URL somewhere?
|
||||
const fullURL = buildFullPath(response.config.baseURL, options.instance.getUri(response.config));
|
||||
|
||||
app.send(
|
||||
Messages.Fetch(
|
||||
typeof response.config.method === 'string' ? response.config.method.toUpperCase() : 'GET',
|
||||
fullURL,
|
||||
requestData,
|
||||
responseData,
|
||||
response.status,
|
||||
startTime + performance.timing.navigationStart,
|
||||
duration,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
options.instance.interceptors.request.use(function (config) {
|
||||
if (options.sessionTokenHeader) {
|
||||
const sessionToken = app.getSessionToken();
|
||||
if (sessionToken) {
|
||||
if (config.headers === undefined) {
|
||||
config.headers = {};
|
||||
}
|
||||
if (config.headers instanceof Headers) {
|
||||
config.headers.append(options.sessionTokenHeader, sessionToken);
|
||||
} else if (Array.isArray(config.headers)) {
|
||||
config.headers.push([options.sessionTokenHeader, sessionToken]);
|
||||
} else {
|
||||
config.headers[options.sessionTokenHeader] = sessionToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.captureWhen(config)) { // TODO: use runWhen as axios publish a version with it
|
||||
// @ts-ignore
|
||||
config.__openreplayStartTs = performance.now();
|
||||
}
|
||||
|
||||
return config;
|
||||
}, function (error) {
|
||||
if (error instanceof Error) {
|
||||
app.send(getExceptionMessage(error, []));
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
// { synchronous: true, /* runWhen: captureWhen // is not published in axios yet */ }
|
||||
);
|
||||
|
||||
options.instance.interceptors.response.use(function (response) {
|
||||
if (!options.failuresOnly) {
|
||||
sendFetchMessage(response);
|
||||
}
|
||||
return response;
|
||||
}, function (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
sendFetchMessage(error.response)
|
||||
} else if (!axios.isCancel(error) && error instanceof Error) {
|
||||
app.send(getExceptionMessage(error, []));
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
24
tracker/tracker-axios/src/url.ts
Normal file
24
tracker/tracker-axios/src/url.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Copied from axios library because these functions haven't been exported.
|
||||
// Why can't axios put constructed fullURL into the config object or in an additional meta information?
|
||||
// TODO: axios feature request
|
||||
|
||||
function isAbsoluteURL(url: string) {
|
||||
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
|
||||
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
|
||||
// by any combination of letters, digits, plus, period, or hyphen.
|
||||
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
|
||||
};
|
||||
|
||||
function combineURLs(baseURL: string, relativeURL: string) {
|
||||
return relativeURL
|
||||
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
|
||||
: baseURL;
|
||||
};
|
||||
|
||||
|
||||
export function buildFullPath(baseURL: string | undefined, requestedURL: string) {
|
||||
if (baseURL && !isAbsoluteURL(requestedURL)) {
|
||||
return combineURLs(baseURL, requestedURL);
|
||||
}
|
||||
return requestedURL;
|
||||
};
|
||||
7
tracker/tracker-axios/tsconfig-cjs.json
Normal file
7
tracker/tracker-axios/tsconfig-cjs.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "./cjs"
|
||||
},
|
||||
}
|
||||
12
tracker/tracker-axios/tsconfig.json
Normal file
12
tracker/tracker-axios/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitThis": true,
|
||||
"strictNullChecks": true,
|
||||
"alwaysStrict": true,
|
||||
"target": "es6",
|
||||
"module": "es6",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"outDir": "./lib"
|
||||
}
|
||||
}
|
||||
34
tracker/tracker-mobx/package-lock.json
generated
34
tracker/tracker-mobx/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@asayerio/tracker-mobx",
|
||||
"version": "5.6.0",
|
||||
"name": "@openreplay/tracker-mobx",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
@ -56,6 +56,15 @@
|
|||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@openreplay/tracker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@openreplay/tracker/-/tracker-3.0.3.tgz",
|
||||
"integrity": "sha512-50C2cwJFENeHNjXVV90uIA5YE1bxfGbhI8e76Nfw9Pg+GVN38DcvGhr3PJ3OKjioT9V4gXBbvtE/RDGRaJJWLA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"error-stack-parser": "^2.0.6"
|
||||
}
|
||||
},
|
||||
"@types/minimist": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz",
|
||||
|
|
@ -191,6 +200,15 @@
|
|||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"error-stack-parser": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz",
|
||||
"integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"stackframe": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
|
|
@ -460,6 +478,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"mobx": {
|
||||
"version": "4.15.7",
|
||||
"resolved": "https://registry.npmjs.org/mobx/-/mobx-4.15.7.tgz",
|
||||
"integrity": "sha512-X4uQvuf2zYKHVO5kRT5Utmr+J9fDnRgxWWnSqJ4oiccPTQU38YG+/O3nPmOhUy4jeHexl7XJJpWDBgEnEfp+8w==",
|
||||
"dev": true
|
||||
},
|
||||
"normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
|
|
@ -694,6 +718,12 @@
|
|||
"integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==",
|
||||
"dev": true
|
||||
},
|
||||
"stackframe": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
|
||||
"integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==",
|
||||
"dev": true
|
||||
},
|
||||
"strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@
|
|||
"dependencies": {},
|
||||
"peerDependencies": {
|
||||
"@openreplay/tracker": "^3.0.0",
|
||||
"mobx": ">= 4.0.0"
|
||||
"mobx": "^4.15.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openreplay/tracker": "^3.0.3",
|
||||
"mobx": "^4.15.7",
|
||||
"prettier": "^1.18.2",
|
||||
"replace-in-files-cli": "^1.0.0",
|
||||
"typescript": "^3.6.4"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"noImplicitThis": true,
|
||||
"strictNullChecks": true,
|
||||
"alwaysStrict": true,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue