Compare commits

...
Sign in to create a new pull request.

12 commits

Author SHA1 Message Date
ourvakan
684399ff1a old heuristics 2021-05-30 17:11:22 +03:00
Shekar Siri
f51cf5ccf2 feat: APIs for user session data deleteion - wip 2021-05-28 14:50:24 +05:30
ShiKhu
daa4489057 fix (tracker-mobx): dev-dependencies and updated package-lock 2021-05-27 22:24:42 +02:00
KRAIEM Taha Yassine
37fdc04693 Fixed alert's unknown metrics handler 2021-05-27 13:38:07 +02:00
ShiKhu
ff1cd1e01f fix (tracker-axios): version patch 2021-05-26 20:20:32 +02:00
ShiKhu
80aca09560 feat (tracker-axios): init plugin 2021-05-26 20:16:56 +02:00
Mehdi Osman
88005ec0d0
Update README.md 2021-05-25 19:45:07 +02:00
Mehdi Osman
af96e5bc26
Fix typo 2021-05-25 18:55:57 +02:00
Rajesh Rajendran
ebc633d147
fix(install): reset ee cli args
Signed-off-by: Rajesh Rajendran <rjshrjndrn@gmail.com>
2021-05-25 19:05:47 +05:30
Rajesh Rajendran
c5c7764212 feat(installation): Enterprise license check 2021-05-25 13:53:20 +05:30
Mehdi Osman
0fb7e4bd07
fix: typo 2021-05-22 02:44:01 +02:00
Rajesh Rajendran
be7c63150b
ci(deployment): injecting secrets
Signed-off-by: Rajesh Rajendran <rjshrjndrn@gmail.com>
2021-05-22 05:48:56 +05:30
52 changed files with 2377 additions and 484 deletions

View file

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

View file

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

@ -83,6 +83,7 @@ wheels/
.installed.cfg
*.egg
MANIFEST
Pipfile
# PyInstaller
# Usually these files are written by a python script from a template

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

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

View 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"
}
}
}

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,4 +6,3 @@ db_list:
- "nfs-server-provisioner"
- "postgresql"
- "redis"
enterprise_edition: false

View file

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

View file

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

View file

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

@ -0,0 +1,6 @@
node_modules
npm-debug.log
lib
cjs
.cache
*.DS_Store

View file

@ -0,0 +1,5 @@
src
tsconfig-cjs.json
tsconfig.json
.prettierrc.json
.cache

View 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.

View 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
View 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"
}
}
}
}

View 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"
}
}

View 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);
});
}
}

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

View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./cjs"
},
}

View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"noImplicitThis": true,
"strictNullChecks": true,
"alwaysStrict": true,
"target": "es6",
"module": "es6",
"moduleResolution": "node",
"declaration": true,
"outDir": "./lib"
}
}

View file

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

View file

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

View file

@ -1,5 +1,6 @@
{
"compilerOptions": {
"skipLibCheck": true,
"noImplicitThis": true,
"strictNullChecks": true,
"alwaysStrict": true,