Compare commits

...

127 commits

Author SHA1 Message Date
Shekar Siri
1324c26429 change(ui): force the table cards events order to use and istead the defaul then 2025-04-23 12:48:46 +02:00
Shekar Siri
9612fa2024 fix(ui): autocomplete options for metadata 2025-04-23 12:26:11 +02:00
GitHub Action
6cbe9632c9 Increment frontend chart version 2025-04-17 11:24:17 +00:00
nick-delirium
46e0903eef ui: fix saved search applier 2025-04-17 14:33:52 +02:00
nick-delirium
dadb1bc01f ui: search fetching 2025-04-17 14:33:52 +02:00
nick-delirium
2cf3243d52 ui: rm load check 2025-04-17 14:33:52 +02:00
nick-delirium
ec27fa2250 ui: patch 1.21 session search 2025-04-17 14:33:52 +02:00
GitHub Action
502e1135f1 Increment frontend chart version 2025-04-14 09:36:51 +00:00
Shekar Siri
fc542f4671 feat(widget-sessions): improve card session filtering logic
- Updated session filtering logic to handle nested filters with null checks.
- Refactored code to improve readability and maintain consistent formatting.
- Enhanced UI components for better user experience, including filter tags
  and pagination.
- Fixed potential issues with duplicate session handling in series filtering.
2025-04-15 16:36:31 +02:00
GitHub Action
1ac90a6059 Increment frontend chart version 2025-04-11 09:40:35 +00:00
Shekar Siri
41b9e38085 feat(ui): improve session count display and filter logic
- Added `flex-shrink-0` class to session count text in
  `CardSessionsByList` for better layout handling.
- Introduced conditional filter logic in `SessionsBy` to handle
  `FETCH` metric with specific `FETCH_URL` filter.
- Removed unused filter mapping logic for cleaner code.

This improves UI consistency and enhances filter functionality.
2025-04-14 11:36:50 +02:00
GitHub Action
6e7a3b22ab Increment chalice chart version 2025-04-10 10:54:28 +00:00
Taha Yassine Kraiem
a319146519 fix(chalice): fixes for table of requests 2025-04-11 11:54:12 +02:00
Taha Yassine Kraiem
075beaf5dc fix(chalice): fixes for table of requests 2025-04-11 11:54:12 +02:00
GitHub Action
7385ed957f Increment frontend chart version 2025-04-10 10:54:27 +00:00
GitHub Action
87085ee191 Increment chalice chart version 2025-04-10 13:59:46 +00:00
GitHub Action
e20043a639 Increment frontend chart version 2025-03-10 10:53:21 +00:00
Shekar Siri
26630b7454 refactor(FilterAutoComplete): optimize component code
- Convert inline function objects to concise syntax
- Properly memoize loadOptions with useCallback
- Add cleanup for debounce function on component unmount
- Fix spacing and remove commented code
- Update debounce implementation to maintain reference stability
2025-04-10 14:31:21 +02:00
Shekar Siri
c499114c78 feat(dates): implement 15-minute interval rounding
Implement time interval rounding to support standardized data queries
by rounding dates to the next 15-minute interval. This change:
- Adds roundToNextInterval method to Search class
- Updates Period.js to round dates to 15-minute intervals
- Updates date range calculations to preserve proper time spans
- Upgrades Sentry browser dependency from v5 to v8.55.0
2025-04-10 12:54:26 +02:00
GitHub Action
83897cb89c Increment frontend chart version 2025-03-10 10:53:20 +00:00
GitHub Action
47411e7f2b Increment chalice chart version 2025-04-08 11:03:20 +00:00
Shekar Siri
6bf370b25f feat(filterItem): improve value handling in toJson method
Enhanced the  method in  to handle various cases for
the  property more robustly. Added checks for empty or invalid
values and adjusted mapping logic for specific filter types like
 and .

- Updated type annotations for better clarity.
- Removed unnecessary trailing commas in imports.
- Improved readability and maintainability of the code.
2025-04-08 12:45:28 +02:00
Shekar Siri
5ec3855491 fix(api): removed extra conditions which has empty filters 2025-04-08 12:45:22 +02:00
rjshrjndrn
d95cfd9ff6 chore(helm): Updating chalice image release 2025-03-10 11:53:19 +01:00
rjshrjndrn
f0d6d8ea12 Updating libxml req 2025-04-03 15:39:46 +02:00
rjshrjndrn
9c57f3db32 chore(helm): Updating chalice image release 2025-04-02 16:56:14 +02:00
Alexander
fa59621482 feat(chalice): added for_spot=True for authenticate_sso 2025-03-11 11:53:18 +01:00
rjshrjndrn
e4ae3c8ba4 feat(helm): add configurable assets origin
Add a helper template to allow customizing the assets origin URL.
This gives users the ability to override the default S3 endpoint
construction when needed, while maintaining backward compatibility.
This can be used when try to use proxy the bucket like cloudfront or
some custom domain.

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-03-10 11:54:32 +01:00
Laurenz Glück
af8cfacd98 fix: updates docker-compose setup to be compatible with v1.21.0 2025-03-07 10:18:08 +01:00
rjshrjndrn
a2ad342a8f refactor(ci): revamp release deployment workflow
Completely redesign the release deployment workflow to:
- Simplify image building and deployment process
- Add branch-based tagging with commit SHA
- Replace AWS ECR login with direct Docker registry auth
- Improve service deployment with explicit image setting
- Update naming and descriptions for better clarity

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-03-06 13:46:01 +01:00
rjshrjndrn
d108e1db82 ci(actions): release build and deployment
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-03-06 13:46:01 +01:00
Aspyryan
51496ae5e2
Running buffer slicing when browser is idle (#3050)
* Fixed tracker uploadOfflineRecording

* Make FlushBuffer perform slicing when browser is idle

* Use map function to cast away proxy objects in flushBuffer
2025-03-03 17:01:35 +01:00
Aspyryan
6b26f85d6e
Fixed tracker uploadOfflineRecording (#3048)
Co-authored-by: Jasper Baetsle <jasper.baetsle@orbid.be>
2025-02-20 17:14:13 +01:00
Mehdi Osman
0ca9327c7b
Increment frontend chart version (#3040)
Co-authored-by: GitHub Action <action@github.com>
2025-02-18 10:15:08 +01:00
Delirium
8914399cad
ui: fix metadata mapper for AssistSessionsModal (#3039) 2025-02-18 10:09:22 +01:00
rjshrjndrn
885c20827e fix(helm): regression #3026
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-02-11 19:12:56 +01:00
rjshrjndrn
8888399359 feat(chalice): if iam role is using then the host variable should be empty
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-02-11 17:22:51 +01:00
rjshrjndrn
eca379feed fix(helm): handle empty s3 endpoint url
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-02-11 16:16:09 +01:00
Kraiem Taha Yassine
f9ec7bce68
chore(actions): changed github actions (#3017) 2025-02-10 12:58:01 +01:00
rjshrjndrn
3975962f70 fix(helm): varable value
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-02-06 11:28:08 +01:00
rjshrjndrn
0780b17a1a chore(helm): Adding secret with db secrets
Use all the db jobs with secret from this.

Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-02-06 11:27:55 +01:00
Mehdi Osman
4b34eceb15
Increment frontend chart version (#3008)
Co-authored-by: GitHub Action <action@github.com>
2025-02-05 12:33:39 +01:00
Shekar Siri
917dad581d
fix(ui): sessions listings settings filter (#3007) 2025-02-05 12:23:16 +01:00
Mehdi Osman
72e42dfd5f
Increment frontend chart version (#2984)
Co-authored-by: GitHub Action <action@github.com>
2025-01-30 09:34:54 +01:00
Shekar Siri
a9a5829000
fix(ui): conditional capture metadata (#2983)
* fix(ui): conditional capture metadata

* change(ui): removed log
2025-01-29 15:46:04 +01:00
Mehdi Osman
1af02eb2c3
Increment chalice chart version (#2981)
Co-authored-by: GitHub Action <action@github.com>
2025-01-28 16:22:39 +01:00
Kraiem Taha Yassine
ac519a0a10
refactor(chalice): return file_key with first mob (#2980) 2025-01-28 16:07:17 +01:00
Mehdi Osman
14f72dbbd7
Updated patch build from main 8326b913f1 (#2978)
* Increment chalice chart version

* Increment frontend chart version

---------

Co-authored-by: GitHub Action <action@github.com>
2025-01-28 13:09:00 +01:00
Shekar Siri
8326b913f1
fix(ui/db): co-browse (assist) list sorting and db default settings for existing users (#2977)
* fix(ui): co-browser (assist) list sorting

* fix(ui): co-browser (assist) list sorting - duration

* fix(ui): default settings values for existing users
2025-01-28 12:54:36 +01:00
Mehdi Osman
fdbed5139c
Increment assets chart version (#2971)
Co-authored-by: GitHub Action <action@github.com>
2025-01-24 14:39:50 +01:00
Alexander
76b0ae7b0d
feat(assets): use content encoding of the original file (#2970) 2025-01-24 14:35:11 +01:00
rjshrjndrn
1a4893bde8 chore(cli): install k3s with cloudflare dns
Signed-off-by: rjshrjndrn <rjshrjndrn@gmail.com>
2025-01-17 16:52:16 +01:00
Mehdi Osman
9e0f309453
Increment frontend chart version (#2946)
Co-authored-by: GitHub Action <action@github.com>
2025-01-13 13:58:59 +01:00
Shekar Siri
262392f32a
fix(ui): user invitation (#2945) 2025-01-13 13:52:44 +01:00
Mehdi Osman
f8a18006d3
Increment chalice chart version (#2940)
Co-authored-by: GitHub Action <action@github.com>
2025-01-10 16:47:14 +01:00
Mehdi Osman
ddaaa1be71
Increment chalice chart version (#2939)
Co-authored-by: GitHub Action <action@github.com>
2025-01-10 16:27:46 +01:00
Mehdi Osman
0417f2c756
Increment chalice chart version (#2938)
Co-authored-by: GitHub Action <action@github.com>
2025-01-10 16:08:48 +01:00
Mehdi Osman
af6dc4b839
Increment chalice chart version (#2937)
Co-authored-by: GitHub Action <action@github.com>
2025-01-10 15:48:27 +01:00
Mehdi Osman
cceb4f0c25
Increment chalice chart version (#2936)
Co-authored-by: GitHub Action <action@github.com>
2025-01-10 15:38:32 +01:00
Mehdi Osman
1f16816252
Increment chalice chart version (#2935)
Co-authored-by: GitHub Action <action@github.com>
2025-01-10 15:28:07 +01:00
Mehdi Osman
77c084cac0
Increment chalice chart version (#2933)
Co-authored-by: GitHub Action <action@github.com>
2025-01-09 17:37:53 -05:00
Kraiem Taha Yassine
c8775f3c15
Patch api v1 21 0 (#2932)
* fix(chalice): fixes for SSO

* fix(chalice): changed base image

* fix(chalice): changed requirements
2025-01-09 19:30:01 +01:00
Mehdi Osman
d52a47e2cc
Increment frontend chart version (#2922)
Co-authored-by: GitHub Action <action@github.com>
2025-01-07 11:12:06 +01:00
Shekar Siri
82854d014d
fix(ui): total/count change as per the api response (#2921) 2025-01-07 10:55:01 +01:00
Mehdi Osman
65d83e91c5
Increment frontend chart version (#2917)
Co-authored-by: GitHub Action <action@github.com>
2025-01-03 10:49:42 +01:00
Shekar Siri
df67acc78f
fix(ui): sessions list and page titles (#2916)
* fix(ui): session tags issue type to be lowercase

* fix(ui): bookmarks separation and other page titles
2025-01-02 15:17:36 +01:00
Mehdi Osman
e5997c662f
Increment frontend chart version (#2915)
Co-authored-by: GitHub Action <action@github.com>
2024-12-30 18:08:55 +01:00
Mehdi Osman
9ae88e62fd
Increment frontend chart version (#2889)
Co-authored-by: GitHub Action <action@github.com>
2024-12-18 10:53:30 +01:00
Delirium
e73bd5fef5
ui: fix sentry log check (#2888) 2024-12-18 10:35:41 +01:00
Mehdi Osman
93dc49bf41
Increment integrations chart version (#2885)
Co-authored-by: GitHub Action <action@github.com>
2024-12-17 15:20:39 +01:00
Alexander
c2a3853bd9
feat(integrations): added missing env configuration for docker-compose and helm-chart (#2884) 2024-12-17 15:15:37 +01:00
Mehdi Osman
5cca953a16
Increment integrations chart version (#2883)
Co-authored-by: GitHub Action <action@github.com>
2024-12-17 11:55:46 +01:00
Alexander
9367c977ca
feat(integrations): fixed a tags search in Sentry provider (#2882) 2024-12-17 11:46:38 +01:00
Mehdi Osman
30dba23530
Increment chalice chart version (#2880)
Co-authored-by: GitHub Action <action@github.com>
2024-12-16 21:20:11 +01:00
Mehdi Osman
21c30af4f0
Increment chalice chart version (#2879)
Co-authored-by: GitHub Action <action@github.com>
2024-12-16 20:08:42 +01:00
Mehdi Osman
da617747dc
Increment chalice chart version (#2878)
Co-authored-by: GitHub Action <action@github.com>
2024-12-16 19:06:10 +01:00
Mehdi Osman
ac2d12d95f
Increment frontend chart version (#2862)
Co-authored-by: GitHub Action <action@github.com>
2024-12-11 18:09:51 +01:00
Mehdi Osman
aa7b3fd617
Increment chalice chart version (#2859)
Co-authored-by: GitHub Action <action@github.com>
2024-12-11 16:38:01 +01:00
Kraiem Taha Yassine
94b541c758
fix(chalice): use card's global filters instead of series' filters (#2857) 2024-12-11 16:17:24 +01:00
Shekar Siri
45ef98b163
fix(ui): funnel - filter sessions by step (#2855) 2024-12-11 14:42:04 +01:00
Mehdi Osman
35eb7d4152
Increment frontend chart version (#2854)
Co-authored-by: GitHub Action <action@github.com>
2024-12-11 13:53:17 +01:00
Delirium
7c23521cb8
ui: fix spot tab lookup, improve js build speed (#2852) 2024-12-11 13:29:38 +01:00
Mehdi Osman
8a8df0a8cb
Increment frontend chart version (#2845)
Co-authored-by: GitHub Action <action@github.com>
2024-12-10 16:38:03 +01:00
Delirium
e54f62a0e6
ui: more fixes... (#2844) 2024-12-10 16:27:59 +01:00
Mehdi Osman
0bdb416594
Increment frontend chart version (#2842)
Co-authored-by: GitHub Action <action@github.com>
2024-12-10 13:44:59 +01:00
Delirium
3484da2f60
Fixup UI crashes (#2840)
* ui: fix log panel crashing

* ui: improve log list filtering

* ui: fix tab name lookup
2024-12-10 13:36:59 +01:00
Mehdi Osman
2f164708e7
Increment frontend chart version (#2838)
Co-authored-by: GitHub Action <action@github.com>
2024-12-10 11:29:37 +01:00
Delirium
c66296a050
Fixup (#2837)
* ui: trim logs

* ui: fixup
2024-12-10 11:14:34 +01:00
Mehdi Osman
a1cf508cb3
Increment frontend chart version (#2836)
Co-authored-by: GitHub Action <action@github.com>
2024-12-10 10:37:39 +01:00
Delirium
38594319f0
Player improvs (#2835)
* ui: fix performance bottlenecks, split data sources in devtools panes

* ui: move xray warn

* Player ux improvements (#2834)

* Player UX improvements.

DevTools (Including multi-tab)
Actions panel (User events, Click maps, Tag Elements)

* ui: remove unused imports, remove str templ classnames

---------

Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>

---------

Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
2024-12-10 10:31:09 +01:00
Shekar Siri
c963ec5e91
Fix card filters for path analysis (#2827)
* fix(ui): card filters restriction for path analysis

* fix(ui): path analysis showing multi series
2024-12-09 13:05:07 +01:00
Shekar Siri
c04090a778
fix(ui): card filters restriction for path analysis (#2826) 2024-12-09 12:40:35 +01:00
Mehdi Osman
fc48ba4149
Increment chalice chart version (#2815)
Co-authored-by: GitHub Action <action@github.com>
2024-12-04 13:10:03 +01:00
Kraiem Taha Yassine
04db322e54
fix(chalice): support webhook default ports (#2814)
* fix(chalice): support webhook default ports

* fix(chalice): support webhook default ports EE
2024-12-04 13:05:38 +01:00
Mehdi Osman
c9ea3651db
Increment frontend chart version (#2813)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 17:16:31 +01:00
Shekar Siri
1dc63bf88b
change(ui): webhooks url allow port (#2812) 2024-12-03 17:09:06 +01:00
Gabriele Angrisani
2fb7b3d542
fix redis volume reference folder (#2805) 2024-12-03 16:33:18 +01:00
Mehdi Osman
57a21eb31d
Increment chalice chart version (#2811)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 16:30:38 +01:00
Kraiem Taha Yassine
e9a1a8c4eb
fix(chalice): fixed edit user's role (#2810) 2024-12-03 16:27:42 +01:00
Mehdi Osman
14191c1de4
Increment chalice chart version (#2807)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 15:36:33 +01:00
Mehdi Osman
7e52c97d62
Increment frontend chart version (#2809)
Co-authored-by: GitHub Action <action@github.com>
2024-12-03 15:35:53 +01:00
Shekar Siri
1cdb9bd06d
fix(ui): do not allow protected roles (#2808) 2024-12-03 14:15:38 +01:00
Kraiem Taha Yassine
e7ad4c8bd0
fix(chalice): support session's search null duration (#2806) 2024-12-03 13:24:24 +01:00
Mehdi Osman
29d69e5b24
Increment chalice chart version (#2804)
Co-authored-by: GitHub Action <action@github.com>
2024-12-02 23:14:07 +01:00
Kraiem Taha Yassine
2e5517509b
fix(chalice): fixed accept invitation response (#2803) 2024-12-02 23:06:41 +01:00
rjshrjndrn
c95a4f6770 ci(action): show latest build log 2024-12-02 21:21:32 +01:00
Mehdi Osman
8af7d1a263
Increment frontend chart version (#2801)
Co-authored-by: GitHub Action <action@github.com>
2024-12-02 21:01:08 +01:00
Shekar Siri
332cbb3516
fix(ui): 403 clear the token (#2800) 2024-12-02 20:55:05 +01:00
Mehdi Osman
1b564f53d5
Increment frontend chart version (#2799)
Co-authored-by: GitHub Action <action@github.com>
2024-12-02 18:24:57 +01:00
Shekar Siri
1aa3b4b4e5
fix(ui): trigger session search on change (#2798) 2024-12-02 18:17:03 +01:00
Mehdi Osman
d531b5da7e
Increment frontend chart version (#2790)
Co-authored-by: GitHub Action <action@github.com>
2024-11-29 16:50:02 +01:00
Shekar Siri
e173591d88
fix(assist): pagination (#2789) 2024-11-29 15:44:45 +01:00
Mehdi Osman
359ecc85af
Increment frontend chart version (#2786)
Co-authored-by: GitHub Action <action@github.com>
2024-11-26 13:11:22 +01:00
Shekar Siri
f0e8100283
fix(ui) - sessions refresh and navigation (#2785)
* change(ui): search query params improvements

* fix(ui): sessions, bookmark, notes navigation and search silters and timestamp issues
2024-11-26 13:03:58 +01:00
Mehdi Osman
251d727375
Increment chalice chart version (#2783)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 17:42:22 +01:00
Kraiem Taha Yassine
b00a90484e
fix(chalice): support user-city for assist (#2782) 2024-11-25 17:35:51 +01:00
Mehdi Osman
ce0686eec3
Increment frontend chart version (#2780)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 14:49:59 +01:00
Shekar Siri
34232ed23c
Fix sessions list (#2779)
* fix(ui): sessions list persist page, show latest sessions

* fix(ui): latest sessions check clear the list
2024-11-25 13:43:07 +01:00
Mehdi Osman
954bfbf8f7
Increment frontend chart version (#2778)
Co-authored-by: GitHub Action <action@github.com>
2024-11-25 12:22:33 +01:00
Shekar Siri
c0197cdfeb
fix(ui): sessions list persist page, show latest sessions (#2777) 2024-11-25 12:12:16 +01:00
Andrés Carrillo
12ab110e0e
Update common.env (#2776) 2024-11-23 10:24:31 -05:00
Mehdi Osman
f48808f42e
Increment frontend chart version (#2774)
Co-authored-by: GitHub Action <action@github.com>
2024-11-22 14:15:28 +01:00
Delirium
b080a98764
ui: fix ws panel crash (#2773) 2024-11-22 14:07:42 +01:00
Mehdi Osman
dd885c65ac
Increment frontend chart version (#2770)
Co-authored-by: GitHub Action <action@github.com>
2024-11-20 15:35:32 +01:00
Shekar Siri
0ad2836650
cherry-pick(ui): 54abbe58a (#2769) 2024-11-20 14:02:42 +01:00
Mehdi Osman
20b76a0ed9
Updated patch build from main 884f3499ef (#2768)
* Increment chalice chart version

* Increment alerts chart version

---------

Co-authored-by: GitHub Action <action@github.com>
2024-11-20 12:45:49 +01:00
Kraiem Taha Yassine
884f3499ef
fix(chalice): support top graphql autocomplete (#2767)
refactor(chalice): enforce UTC TZ
refactor(crons): enforce UTC TZ
refactor(alerts): enforce UTC TZ
2024-11-20 12:34:25 +01:00
158 changed files with 3253 additions and 1778 deletions

View file

@ -10,8 +10,6 @@ on:
branches:
- dev
- api-*
- v1.11.0-patch
- actions_test
paths:
- "ee/api/**"
- "api/**"

View file

@ -10,7 +10,6 @@ on:
branches:
- dev
- api-*
- v1.11.0-patch
paths:
- "api/**"
- "!api/.gitignore"

View file

@ -9,7 +9,6 @@ on:
push:
branches:
- dev
- api-*
paths:
- "ee/assist/**"
- "assist/**"

View file

@ -9,7 +9,6 @@ on:
push:
branches:
- dev
- api-*
paths:
- "assist/**"
- "!assist/.gitignore"

View file

@ -10,7 +10,6 @@ on:
branches:
- dev
- api-*
- v1.11.0-patch
paths:
- "ee/api/**"
- "api/**"

View file

@ -83,6 +83,7 @@ jobs:
[ -d $MSAAS_REPO_FOLDER ] || {
git clone -b dev --recursive https://x-access-token:$MSAAS_REPO_CLONE_TOKEN@$MSAAS_REPO_URL $MSAAS_REPO_FOLDER
cd $MSAAS_REPO_FOLDER
git log -1
cd openreplay && git fetch origin && git checkout main # This have to be changed to specific tag
git log -1
cd $MSAAS_REPO_FOLDER

View file

@ -9,7 +9,6 @@ on:
push:
branches:
- dev
- api-*
paths:
- "ee/peers/**"
- "peers/**"

View file

@ -9,7 +9,6 @@ on:
push:
branches:
- dev
- api-*
paths:
- "peers/**"
- "!peers/.gitignore"

View file

@ -0,0 +1,92 @@
name: Release Deployment
on:
workflow_dispatch:
inputs:
services:
description: 'Comma-separated list of services to deploy'
required: true
branch:
description: 'Branch to deploy (defaults to dev)'
required: false
default: 'dev'
env:
IMAGE_REGISTRY_URL: ${{ secrets.OSS_REGISTRY_URL }}
DEPOT_PROJECT_ID: ${{ secrets.DEPOT_PROJECT_ID }}
DOCKER_REPO_OSS: ${{ secrets.OSS_REGISTRY_URL }}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.branch }}
- name: Docker login
run: |
docker login ${{ secrets.OSS_REGISTRY_URL }} -u ${{ secrets.OSS_DOCKER_USERNAME }} -p "${{ secrets.OSS_REGISTRY_TOKEN }}"
- name: Set image tag with branch info
run: |
SHORT_SHA=$(git rev-parse --short HEAD)
echo "IMAGE_TAG=${IMAGE_TAG}-${{ github.event.inputs.branch }}-${SHORT_SHA}" >> $GITHUB_ENV
echo "Using image tag: ${IMAGE_TAG}-${{ github.event.inputs.branch }}-${SHORT_SHA}"
- uses: depot/setup-action@v1
- name: Build and push Docker images
run: |
# Parse the comma-separated services list into an array
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
# Define backend services (consider moving this to workflow inputs or repo config)
ls backend/cmd >> /tmp/backend.txt
for SERVICE in "${SERVICES[@]}"; do
# Check if service is backend
if grep -q $SERVICE /tmp/backend.txt; then
cd backend
foss_build_args="nil $SERVICE"
ee_build_args="ee $SERVICE"
else
[[ $SERVICE == 'chalice' || $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && cd $working_dir/api || cd $SERVICE
[[ $SERVICE == 'alerts' || $SERVICE == 'crons' ]] && BUILD_SCRIPT_NAME="build_${SERVICE}.sh"
ee_build_args="ee"
fi
echo IMAGE_TAG=$IMAGE_TAG DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
IMAGE_TAG=$IMAGE_TAG DOCKER_RUNTIME="depot" DOCKER_BUILD_ARGS="--push" ARCH=amd64 DOCKER_REPO=$DOCKER_REPO_OSS PUSH_IMAGE=0 bash ${BUILD_SCRIPT_NAME} $foss_build_args
done
- uses: azure/k8s-set-context@v1
name: Using ee release cluster
with:
method: kubeconfig
kubeconfig: ${{ secrets.EE_RELEASE_KUBECONFIG }}
- name: Deploy to ee release Kubernetes
run: |
echo "Deploying services to EE cluster: ${{ github.event.inputs.services }}"
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
for SERVICE in "${SERVICES[@]}"; do
SERVICE=$(echo $SERVICE | xargs) # Trim whitespace
echo "Deploying $SERVICE to EE cluster with image tag: ${IMAGE_TAG}"
kubectl set image deployment/$SERVICE-openreplay -n app $SERVICE=${{ secrets.RELEASE_OSS_REGISTRY }}/$SERVICE:${IMAGE_TAG}
done
- uses: azure/k8s-set-context@v1
name: Using foss release cluster
with:
method: kubeconfig
kubeconfig: ${{ secrets.FOSS_RELEASE_KUBECONFIG }}
- name: Deploy to FOSS release Kubernetes
run: |
echo "Deploying services to FOSS cluster: ${{ github.event.inputs.services }}"
IFS=',' read -ra SERVICES <<< "${{ github.event.inputs.services }}"
for SERVICE in "${SERVICES[@]}"; do
SERVICE=$(echo $SERVICE | xargs) # Trim whitespace
echo "Deploying $SERVICE to FOSS cluster with image tag: ${IMAGE_TAG}"
kubectl set image deployment/$SERVICE-openreplay -n app $SERVICE=${{ secrets.RELEASE_OSS_REGISTRY }}/$SERVICE:${IMAGE_TAG}
done

View file

@ -9,7 +9,6 @@ on:
push:
branches:
- dev
- api-*
paths:
- "sourcemap-reader/**"
- "!sourcemap-reader/.gitignore"

View file

@ -9,7 +9,6 @@ on:
push:
branches:
- dev
- api-*
paths:
- "sourcemap-reader/**"
- "!sourcemap-reader/.gitignore"

View file

@ -313,7 +313,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
"isEvent": True,
"value": [],
"operator": e.operator,
"filters": []
"filters": e.filters
})
for v in e.value:
if v not in extra_conditions[e.operator].value:
@ -330,7 +330,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
"isEvent": True,
"value": [],
"operator": e.operator,
"filters": []
"filters": e.filters
})
for v in e.value:
if v not in extra_conditions[e.operator].value:
@ -1156,7 +1156,7 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status,
sh.multi_conditions(f"ev.{events.EventType.LOCATION.column} {op} %({e_k})s",
c.value, value_key=e_k))
else:
logger.warning(f"unsupported extra_event type:${c.type}")
logger.warning(f"unsupported extra_event type: {c.type}")
if len(_extra_or_condition) > 0:
extra_constraints.append("(" + " OR ".join(_extra_or_condition) + ")")
query_part = f"""\

View file

@ -457,12 +457,6 @@ def set_password_invitation(user_id, new_password):
user = update(tenant_id=-1, user_id=user_id, changes=changes)
r = authenticate(user['email'], new_password)
tenant_id = r.pop("tenantId")
r["limits"] = {
"teamMember": -1,
"projects": -1,
"metadata": metadata.get_remaining_metadata_with_count(tenant_id)}
return {
"jwt": r.pop("jwt"),
"refreshToken": r.pop("refreshToken"),
@ -470,10 +464,7 @@ def set_password_invitation(user_id, new_password):
"spotJwt": r.pop("spotJwt"),
"spotRefreshToken": r.pop("spotRefreshToken"),
"spotRefreshTokenMaxAge": r.pop("spotRefreshTokenMaxAge"),
'data': {
"scopeState": scope.get_scope(-1),
"user": r
}
**r
}

View file

@ -129,13 +129,13 @@ def add_edit(tenant_id, data: schemas.WebhookSchema, replace_none=None):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"name already exists.")
if data.webhook_id is not None:
return update(tenant_id=tenant_id, webhook_id=data.webhook_id,
changes={"endpoint": data.endpoint.unicode_string(),
changes={"endpoint": data.endpoint,
"authHeader": data.auth_header,
"name": data.name},
replace_none=replace_none)
else:
return add(tenant_id=tenant_id,
endpoint=data.endpoint.unicode_string(),
endpoint=data.endpoint,
auth_header=data.auth_header,
name=data.name,
replace_none=replace_none)

View file

@ -1,35 +1,41 @@
from typing import Union
from enum import Enum
import schemas
def get_sql_operator(op: Union[schemas.SearchEventOperator, schemas.ClickEventExtraOperator]):
if isinstance(op, Enum):
op = op.value
return {
schemas.SearchEventOperator.IS: "=",
schemas.SearchEventOperator.ON: "=",
schemas.SearchEventOperator.ON_ANY: "IN",
schemas.SearchEventOperator.IS_NOT: "!=",
schemas.SearchEventOperator.NOT_ON: "!=",
schemas.SearchEventOperator.CONTAINS: "ILIKE",
schemas.SearchEventOperator.NOT_CONTAINS: "NOT ILIKE",
schemas.SearchEventOperator.STARTS_WITH: "ILIKE",
schemas.SearchEventOperator.ENDS_WITH: "ILIKE",
schemas.SearchEventOperator.IS.value: "=",
schemas.SearchEventOperator.ON.value: "=",
schemas.SearchEventOperator.ON_ANY.value: "IN",
schemas.SearchEventOperator.IS_NOT.value: "!=",
schemas.SearchEventOperator.NOT_ON.value: "!=",
schemas.SearchEventOperator.CONTAINS.value: "ILIKE",
schemas.SearchEventOperator.NOT_CONTAINS.value: "NOT ILIKE",
schemas.SearchEventOperator.STARTS_WITH.value: "ILIKE",
schemas.SearchEventOperator.ENDS_WITH.value: "ILIKE",
# Selector operators:
schemas.ClickEventExtraOperator.IS: "=",
schemas.ClickEventExtraOperator.IS_NOT: "!=",
schemas.ClickEventExtraOperator.CONTAINS: "ILIKE",
schemas.ClickEventExtraOperator.NOT_CONTAINS: "NOT ILIKE",
schemas.ClickEventExtraOperator.STARTS_WITH: "ILIKE",
schemas.ClickEventExtraOperator.ENDS_WITH: "ILIKE",
schemas.ClickEventExtraOperator.IS.value: "=",
schemas.ClickEventExtraOperator.IS_NOT.value: "!=",
schemas.ClickEventExtraOperator.CONTAINS.value: "ILIKE",
schemas.ClickEventExtraOperator.NOT_CONTAINS.value: "NOT ILIKE",
schemas.ClickEventExtraOperator.STARTS_WITH.value: "ILIKE",
schemas.ClickEventExtraOperator.ENDS_WITH.value: "ILIKE",
}.get(op, "=")
def is_negation_operator(op: schemas.SearchEventOperator):
return op in [schemas.SearchEventOperator.IS_NOT,
schemas.SearchEventOperator.NOT_ON,
schemas.SearchEventOperator.NOT_CONTAINS,
schemas.ClickEventExtraOperator.IS_NOT,
schemas.ClickEventExtraOperator.NOT_CONTAINS]
if isinstance(op, Enum):
op = op.value
return op in [schemas.SearchEventOperator.IS_NOT.value,
schemas.SearchEventOperator.NOT_ON.value,
schemas.SearchEventOperator.NOT_CONTAINS.value,
schemas.ClickEventExtraOperator.IS_NOT.value,
schemas.ClickEventExtraOperator.NOT_CONTAINS.value]
def reverse_sql_operator(op):

View file

@ -1,3 +1,4 @@
#!/bin/sh
export TZ=UTC
uvicorn app:app --host 0.0.0.0 --port $LISTEN_PORT --proxy-headers --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore
uvicorn app:app --host 0.0.0.0 --port 8888 --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/zsh
export TZ=UTC
uvicorn app_alerts:app --reload --port 8888 --log-level ${S_LOGLEVEL:-warning}

View file

@ -1,3 +1,4 @@
#!/bin/zsh
export TZ=UTC
uvicorn app:app --reload --log-level ${S_LOGLEVEL:-warning}

View file

@ -211,7 +211,8 @@ class IssueTrackingJiraSchema(IssueTrackingIntegration):
class WebhookSchema(BaseModel):
webhook_id: Optional[int] = Field(default=None)
endpoint: AnyHttpUrl = Field(...)
processed_endpoint: AnyHttpUrl = Field(..., alias="endpoint")
endpoint: Optional[str] = Field(default=None, doc_hidden=True)
auth_header: Optional[str] = Field(default=None)
name: str = Field(default="", max_length=100, pattern=NAME_PATTERN)
@ -754,6 +755,8 @@ class SessionsSearchPayloadSchema(_TimedSchema, _PaginatedSchema):
for f in values.get("filters", []):
vals = []
for v in f.get("value", []):
if f.get("type", "") == FilterType.DURATION.value and v is None:
v = 0
if v is not None and (f.get("type", "") != FilterType.DURATION.value
or str(v).isnumeric()):
vals.append(v)
@ -1020,33 +1023,51 @@ class CardSessionsSchema(_TimedSchema, _PaginatedSchema):
return self
# We don't need this as the UI is expecting filters to override the full series' filters
# @model_validator(mode="after")
# def __merge_out_filters_with_series(self):
# for f in self.filters:
# for s in self.series:
# found = False
#
# if f.is_event:
# sub = s.filter.events
# else:
# sub = s.filter.filters
#
# for e in sub:
# if f.type == e.type and f.operator == e.operator:
# found = True
# if f.is_event:
# # If extra event: append value
# for v in f.value:
# if v not in e.value:
# e.value.append(v)
# else:
# # If extra filter: override value
# e.value = f.value
# if not found:
# sub.append(f)
#
# self.filters = []
#
# return self
# UI is expecting filters to override the full series' filters
@model_validator(mode="after")
def __merge_out_filters_with_series(self):
for f in self.filters:
for s in self.series:
found = False
def __override_series_filters_with_outer_filters(self):
if len(self.filters) > 0:
events = []
filters = []
for f in self.filters:
if f.is_event:
sub = s.filter.events
events.append(f)
else:
sub = s.filter.filters
for e in sub:
if f.type == e.type and f.operator == e.operator:
found = True
if f.is_event:
# If extra event: append value
for v in f.value:
if v not in e.value:
e.value.append(v)
else:
# If extra filter: override value
e.value = f.value
if not found:
sub.append(f)
filters.append(f)
for s in self.series:
s.filter.events = events
s.filter.filters = filters
self.filters = []
return self
@ -1357,6 +1378,7 @@ class LiveFilterType(str, Enum):
USER_BROWSER = FilterType.USER_BROWSER.value
USER_DEVICE = FilterType.USER_DEVICE.value
USER_COUNTRY = FilterType.USER_COUNTRY.value
USER_CITY = FilterType.USER_CITY.value
USER_STATE = FilterType.USER_STATE.value
USER_ID = FilterType.USER_ID.value
USER_ANONYMOUS_ID = FilterType.USER_ANONYMOUS_ID.value

View file

@ -143,6 +143,7 @@ func (c *cacher) cacheURL(t *Task) {
if contentType == "" {
contentType = mime.TypeByExtension(filepath.Ext(res.Request.URL.Path))
}
contentEncoding := res.Header.Get("Content-Encoding")
// Skip html file (usually it's a CDN mock for 404 error)
if strings.HasPrefix(contentType, "text/html") {
@ -159,7 +160,7 @@ func (c *cacher) cacheURL(t *Task) {
// TODO: implement in streams
start = time.Now()
err = c.objStorage.Upload(strings.NewReader(strData), t.cachePath, contentType, objectstorage.NoCompression)
err = c.objStorage.Upload(strings.NewReader(strData), t.cachePath, contentType, contentEncoding, objectstorage.NoCompression)
if err != nil {
metrics.RecordUploadDuration(float64(time.Now().Sub(start).Milliseconds()), true)
c.Errors <- errors.Wrap(err, t.urlContext)

View file

@ -385,7 +385,7 @@ func (s *Storage) uploadSession(payload interface{}) {
metrics.RecordSessionCompressionRatio(task.domsRawSize/float64(task.doms.Len()), DOM.String())
// Upload session to s3
start := time.Now()
if err := s.objStorage.Upload(task.doms, task.id+string(DOM)+"s", "application/octet-stream", task.compression); err != nil {
if err := s.objStorage.Upload(task.doms, task.id+string(DOM)+"s", "application/octet-stream", "", task.compression); err != nil {
s.log.Fatal(task.ctx, "failed to upload mob file, err: %s", err)
}
uploadDoms = time.Now().Sub(start).Milliseconds()
@ -398,7 +398,7 @@ func (s *Storage) uploadSession(payload interface{}) {
metrics.RecordSessionCompressionRatio(task.domeRawSize/float64(task.dome.Len()), DOM.String())
// Upload session to s3
start := time.Now()
if err := s.objStorage.Upload(task.dome, task.id+string(DOM)+"e", "application/octet-stream", task.compression); err != nil {
if err := s.objStorage.Upload(task.dome, task.id+string(DOM)+"e", "application/octet-stream", "", task.compression); err != nil {
s.log.Fatal(task.ctx, "failed to upload mob file, err: %s", err)
}
uploadDome = time.Now().Sub(start).Milliseconds()
@ -411,7 +411,7 @@ func (s *Storage) uploadSession(payload interface{}) {
metrics.RecordSessionCompressionRatio(task.devRawSize/float64(task.dev.Len()), DEV.String())
// Upload session to s3
start := time.Now()
if err := s.objStorage.Upload(task.dev, task.id+string(DEV), "application/octet-stream", task.compression); err != nil {
if err := s.objStorage.Upload(task.dev, task.id+string(DEV), "application/octet-stream", "", task.compression); err != nil {
s.log.Fatal(task.ctx, "failed to upload mob file, err: %s", err)
}
uploadDev = time.Now().Sub(start).Milliseconds()

View file

@ -48,12 +48,12 @@ func (s *sentryClient) FetchSessionData(credentials interface{}, sessionID uint6
cfg.Token = val
}
}
requestUrl := fmt.Sprintf("https://sentry.io/api/0/projects/%s/%s/events/", cfg.OrganizationSlug, cfg.ProjectSlug)
requestUrl := fmt.Sprintf("https://sentry.io/api/0/projects/%s/%s/issues/", cfg.OrganizationSlug, cfg.ProjectSlug)
testCallLimit := 1
params := url.Values{}
if sessionID != 0 {
params.Add("query", fmt.Sprintf("openReplaySession.id=%d", sessionID))
params.Add("query", fmt.Sprintf("openReplaySession.id:%d", sessionID))
} else {
params.Add("per_page", fmt.Sprintf("%d", testCallLimit))
}

View file

@ -15,7 +15,7 @@ const (
)
type ObjectStorage interface {
Upload(reader io.Reader, key string, contentType string, compression CompressionType) error
Upload(reader io.Reader, key string, contentType, contentEncoding string, compression CompressionType) error
Get(key string) (io.ReadCloser, error)
Exists(key string) bool
GetCreationTime(key string) *time.Time

View file

@ -67,19 +67,22 @@ func NewS3(cfg *objConfig.ObjectsConfig) (objectstorage.ObjectStorage, error) {
}, nil
}
func (s *storageImpl) Upload(reader io.Reader, key string, contentType string, compression objectstorage.CompressionType) error {
func (s *storageImpl) Upload(reader io.Reader, key string, contentType, contentEncoding string, compression objectstorage.CompressionType) error {
cacheControl := "max-age=2628000, immutable, private"
var contentEncoding *string
var encoding *string
switch compression {
case objectstorage.Gzip:
encodeStr := "gzip"
contentEncoding = &encodeStr
encoding = &encodeStr
case objectstorage.Brotli:
encodeStr := "br"
contentEncoding = &encodeStr
encoding = &encodeStr
case objectstorage.Zstd:
// Have to ignore contentEncoding for Zstd (otherwise will be an error in browser)
}
if contentEncoding != "" {
encoding = &contentEncoding
}
_, err := s.uploader.Upload(&s3manager.UploadInput{
Body: reader,
@ -87,7 +90,7 @@ func (s *storageImpl) Upload(reader io.Reader, key string, contentType string, c
Key: &key,
ContentType: &contentType,
CacheControl: &cacheControl,
ContentEncoding: contentEncoding,
ContentEncoding: encoding,
Tagging: s.fileTag,
})
return err

View file

@ -1,8 +1,31 @@
FROM python:3.11-alpine
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
RUN apk add --no-cache build-base libressl libffi-dev libressl-dev libxslt-dev libxml2-dev xmlsec-dev xmlsec tini
FROM python:3.12-alpine
LABEL maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
WORKDIR /app
COPY . .
RUN mv env.default .env
RUN apk add --no-cache tini xmlsec && \
export UV_SYSTEM_PYTHON=true && \
pip install uv && \
apk add --no-cache --virtual .build-deps \
build-base \
libressl \
libffi-dev \
libressl-dev \
libxslt-dev \
libxml2-dev \
xmlsec-dev && \
uv pip install --no-cache-dir --upgrade -r requirements.txt && \
# Solve the libxml2 library version mismatch by reinstalling lxml with matching libxml2
uv pip uninstall lxml && \
uv pip install --no-cache-dir --no-binary lxml lxml --force-reinstall && \
# Create non-root user
adduser -u 1001 openreplay -D && \
# Cleanup build dependencies
apk del .build-deps
ARG envarg
ARG GIT_SHA
ENV SOURCE_MAP_VERSION=0.7.4 \
APP_NAME=chalice \
LISTEN_PORT=8000 \
@ -10,19 +33,12 @@ ENV SOURCE_MAP_VERSION=0.7.4 \
ENTERPRISE_BUILD=${envarg} \
GIT_SHA=$GIT_SHA
WORKDIR /work
COPY requirements.txt ./requirements.txt
# Caching the source build
RUN pip install --no-cache-dir --upgrade uv
RUN uv pip install --no-cache-dir --upgrade pip setuptools wheel --system
RUN uv pip install --no-cache-dir --upgrade python3-saml==1.16.0 --no-binary=lxml --system
RUN uv pip install --no-cache-dir --upgrade -r requirements.txt --system
WORKDIR /app
COPY . .
RUN mv env.default .env
RUN adduser -u 1001 openreplay -D
USER 1001
ENTRYPOINT ["/sbin/tini", "--"]
CMD ./entrypoint.sh
CMD ["./entrypoint.sh"]

View file

@ -1,4 +1,4 @@
FROM python:3.11-alpine
FROM python:3.12-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
RUN apk add --no-cache build-base tini

View file

@ -1,4 +1,4 @@
FROM python:3.11-alpine
FROM python:3.12-alpine
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
RUN apk add --no-cache build-base tini

View file

@ -20,6 +20,7 @@ python-decouple = "==3.8"
pydantic = {extras = ["email"], version = "==2.9.2"}
apscheduler = "==3.10.4"
clickhouse-driver = {extras = ["lz4"], version = "==0.2.9"}
lxml = "!=4.7.0,<=5.2.1,>=4.6.5"
python3-saml = "==1.16.0"
python-multipart = "==0.0.17"
redis = "==5.2.0"

View file

@ -182,3 +182,20 @@ def delete(tenant_id, user_id, role_id):
{"tenant_id": tenant_id, "role_id": role_id})
cur.execute(query=query)
return get_roles(tenant_id=tenant_id)
def get_role(tenant_id, role_id):
with pg_client.PostgresClient() as cur:
query = cur.mogrify("""SELECT roles.*
FROM public.roles
WHERE tenant_id =%(tenant_id)s
AND deleted_at IS NULL
AND not service_role
AND role_id = %(role_id)s
LIMIT 1;""",
{"tenant_id": tenant_id, "role_id": role_id})
cur.execute(query=query)
row = cur.fetchone()
if row is not None:
row["created_at"] = TimeUTC.datetime_to_timestamp(row["created_at"])
return helper.dict_to_camel_case(row)

View file

@ -363,7 +363,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
"isEvent": True,
"value": [],
"operator": e.operator,
"filters": []
"filters": e.filters
})
for v in e.value:
if v not in extra_conditions[e.operator].value:
@ -386,7 +386,7 @@ def search2_table(data: schemas.SessionsSearchPayloadSchema, project_id: int, de
"isEvent": True,
"value": [],
"operator": e.operator,
"filters": []
"filters": e.filters
})
for v in e.value:
if v not in extra_conditions[e.operator].value:
@ -506,7 +506,7 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
data.filters.append(
schemas.SessionSearchFilterSchema(value=[issue['type']],
type=schemas.FilterType.ISSUE.value,
operator='is')
operator=schemas.SearchEventOperator.IS)
)
ss_constraints = []
full_args = {"project_id": project_id, "startDate": data.startTimestamp, "endDate": data.endTimestamp,
@ -1200,8 +1200,12 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
is_any = _isAny_opreator(f.operator)
if is_any or len(f.value) == 0:
continue
is_negative_operator = sh.is_negation_operator(f.operator)
f.value = helper.values_for_operator(value=f.value, op=f.operator)
op = sh.get_sql_operator(f.operator)
r_op = ""
if is_negative_operator:
r_op = sh.reverse_sql_operator(op)
e_k_f = e_k + f"_fetch{j}"
full_args = {**full_args, **_multiple_values(f.value, value_key=e_k_f)}
if f.type == schemas.FetchFilterType.FETCH_URL:
@ -1210,6 +1214,13 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
value_key=e_k_f))
events_conditions[-1]["condition"].append(event_where[-1])
apply = True
if is_negative_operator:
events_conditions_not.append(
{
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
events_conditions_not[-1]["condition"] = _multiple_conditions(
f"sub.url_path {r_op} %({e_k_f})s", f.value,
value_key=e_k_f)
elif f.type == schemas.FetchFilterType.FETCH_STATUS_CODE:
event_where.append(
_multiple_conditions(f"main.status {f.operator} %({e_k_f})s", f.value,
@ -1221,6 +1232,13 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
_multiple_conditions(f"main.method {op} %({e_k_f})s", f.value, value_key=e_k_f))
events_conditions[-1]["condition"].append(event_where[-1])
apply = True
if is_negative_operator:
events_conditions_not.append(
{
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
events_conditions_not[-1]["condition"] = _multiple_conditions(
f"sub.method {r_op} %({e_k_f})s", f.value,
value_key=e_k_f)
elif f.type == schemas.FetchFilterType.FETCH_DURATION:
event_where.append(
_multiple_conditions(f"main.duration {f.operator} %({e_k_f})s", f.value,
@ -1232,11 +1250,25 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
_multiple_conditions(f"main.request_body {op} %({e_k_f})s", f.value, value_key=e_k_f))
events_conditions[-1]["condition"].append(event_where[-1])
apply = True
if is_negative_operator:
events_conditions_not.append(
{
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
events_conditions_not[-1]["condition"] = _multiple_conditions(
f"sub.request_body {r_op} %({e_k_f})s", f.value,
value_key=e_k_f)
elif f.type == schemas.FetchFilterType.FETCH_RESPONSE_BODY:
event_where.append(
_multiple_conditions(f"main.response_body {op} %({e_k_f})s", f.value, value_key=e_k_f))
events_conditions[-1]["condition"].append(event_where[-1])
apply = True
if is_negative_operator:
events_conditions_not.append(
{
"type": f"sub.event_type='{exp_ch_helper.get_event_type(event_type, platform=platform)}'"})
events_conditions_not[-1]["condition"] = _multiple_conditions(
f"sub.response_body {r_op} %({e_k_f})s", f.value,
value_key=e_k_f)
else:
logging.warning(f"undefined FETCH filter: {f.type}")
if not apply:
@ -1481,19 +1513,32 @@ def search_query_parts_ch(data: schemas.SessionsSearchPayloadSchema, error_statu
if extra_conditions and len(extra_conditions) > 0:
_extra_or_condition = []
for i, c in enumerate(extra_conditions):
if _isAny_opreator(c.operator):
if _isAny_opreator(c.operator) and c.type != schemas.EventType.REQUEST_DETAILS.value:
continue
e_k = f"ec_value{i}"
op = sh.get_sql_operator(c.operator)
c.value = helper.values_for_operator(value=c.value, op=c.operator)
full_args = {**full_args,
**_multiple_values(c.value, value_key=e_k)}
if c.type == events.EventType.LOCATION.ui_type:
if c.type in (schemas.EventType.LOCATION.value, schemas.EventType.REQUEST.value):
_extra_or_condition.append(
_multiple_conditions(f"extra_event.url_path {op} %({e_k})s",
c.value, value_key=e_k))
elif c.type == schemas.EventType.REQUEST_DETAILS.value:
for j, c_f in enumerate(c.filters):
if _isAny_opreator(c_f.operator) or len(c_f.value) == 0:
continue
e_k += f"_{j}"
op = sh.get_sql_operator(c_f.operator)
c_f.value = helper.values_for_operator(value=c_f.value, op=c_f.operator)
full_args = {**full_args,
**_multiple_values(c_f.value, value_key=e_k)}
if c_f.type == schemas.FetchFilterType.FETCH_URL.value:
_extra_or_condition.append(
_multiple_conditions(f"extra_event.url_path {op} %({e_k})s",
c_f.value, value_key=e_k))
else:
logging.warning(f"unsupported extra_event type:${c.type}")
logging.warning(f"unsupported extra_event type:{c.type}")
if len(_extra_or_condition) > 0:
extra_constraints.append("(" + " OR ".join(_extra_or_condition) + ")")
else:

View file

@ -19,7 +19,23 @@ def __group_metadata(session, project_metadata):
def get_pre_replay(project_id, session_id):
with pg_client.PostgresClient() as cur:
query = cur.mogrify(
f"""\
SELECT encode(file_key,'hex') AS file_key
FROM public.sessions
WHERE project_id = %(project_id)s
AND session_id = %(session_id)s;""",
{"project_id": project_id, "session_id": session_id}
)
cur.execute(query=query)
data = cur.fetchone()
file_key = None
if data is not None:
file_key = data['file_key']
return {
'fileKey': file_key,
'domURL': [sessions_mobs.get_first_url(project_id=project_id, session_id=session_id, check_existence=False)]}

View file

@ -199,6 +199,12 @@ def create_member(tenant_id, user_id, data: schemas.CreateMemberSchema, backgrou
role_id = data.roleId
if role_id is None:
role_id = roles.get_role_by_name(tenant_id=tenant_id, name="member").get("roleId")
else:
role = roles.get_role(tenant_id=tenant_id, role_id=role_id)
if role is None:
return {"errors": ["role not found"]}
if role["name"].lower() == "owner" and role["protected"]:
return {"errors": ["invalid role"]}
invitation_token = __generate_invitation_token()
user = get_deleted_user_by_email(email=data.email)
if user is not None and user["tenantId"] == tenant_id:
@ -333,7 +339,7 @@ def edit_member(user_id_to_update, tenant_id, changes: schemas.EditMemberSchema,
if editor_id != user_id_to_update:
admin = get_user_role(tenant_id=tenant_id, user_id=editor_id)
if not admin["superAdmin"] and not admin["admin"]:
return {"errors": ["unauthorized"]}
return {"errors": ["unauthorized, you must have admin privileges"]}
if admin["admin"] and user["superAdmin"]:
return {"errors": ["only the owner can edit his own details"]}
else:
@ -343,10 +349,10 @@ def edit_member(user_id_to_update, tenant_id, changes: schemas.EditMemberSchema,
return {"errors": ["cannot change your own admin privileges"]}
if changes.roleId:
if user["superAdmin"] and changes.roleId != user["roleId"]:
changes.roleId = None
return {"errors": ["owner's role cannot be changed"]}
if changes.roleId != user["roleId"]:
elif user["superAdmin"]:
changes.roleId = None
elif changes.roleId != user["roleId"]:
return {"errors": ["cannot change your own role"]}
if changes.name and len(changes.name) > 0:
@ -357,6 +363,12 @@ def edit_member(user_id_to_update, tenant_id, changes: schemas.EditMemberSchema,
if changes.roleId is not None:
_changes["roleId"] = changes.roleId
role = roles.get_role(tenant_id=tenant_id, role_id=changes.roleId)
if role is None:
return {"errors": ["role not found"]}
else:
if role["name"].lower() == "owner" and role["protected"]:
return {"errors": ["invalid role"]}
if len(_changes.keys()) > 0:
update(tenant_id=tenant_id, user_id=user_id_to_update, changes=_changes, output=False)
@ -540,12 +552,6 @@ def set_password_invitation(tenant_id, user_id, new_password):
user = update(tenant_id=tenant_id, user_id=user_id, changes=changes)
r = authenticate(user['email'], new_password)
tenant_id = r.pop("tenantId")
r["limits"] = {
"teamMember": -1,
"projects": -1,
"metadata": metadata.get_remaining_metadata_with_count(tenant_id)}
return {
"jwt": r.pop("jwt"),
"refreshToken": r.pop("refreshToken"),
@ -554,10 +560,7 @@ def set_password_invitation(tenant_id, user_id, new_password):
"spotRefreshToken": r.pop("spotRefreshToken"),
"spotRefreshTokenMaxAge": r.pop("spotRefreshTokenMaxAge"),
"tenantId": tenant_id,
'data': {
"scopeState": scope.get_scope(tenant_id),
"user": r
}
**r
}
@ -899,12 +902,12 @@ def authenticate_sso(email: str, internal_id: str):
aud=AUDIENCE, jwt_jti=j_r.jwt_refresh_jti),
"refreshTokenMaxAge": config("JWT_REFRESH_EXPIRATION", cast=int),
"spotJwt": authorizers.generate_jwt(user_id=r['userId'], tenant_id=r['tenantId'],
iat=j_r.spot_jwt_iat, aud=spot.AUDIENCE),
iat=j_r.spot_jwt_iat, aud=spot.AUDIENCE, for_spot=True),
"spotRefreshToken": authorizers.generate_jwt_refresh(user_id=r['userId'],
tenant_id=r['tenantId'],
iat=j_r.spot_jwt_refresh_iat,
aud=spot.AUDIENCE,
jwt_jti=j_r.spot_jwt_refresh_jti),
jwt_jti=j_r.spot_jwt_refresh_jti, for_spot=True),
"spotRefreshTokenMaxAge": config("JWT_SPOT_REFRESH_EXPIRATION", cast=int)
}
return response

View file

@ -136,13 +136,13 @@ def add_edit(tenant_id, data: schemas.WebhookSchema, replace_none=None):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"name already exists.")
if data.webhook_id is not None:
return update(tenant_id=tenant_id, webhook_id=data.webhook_id,
changes={"endpoint": data.endpoint.unicode_string(),
changes={"endpoint": data.endpoint,
"authHeader": data.auth_header,
"name": data.name},
replace_none=replace_none)
else:
return add(tenant_id=tenant_id,
endpoint=data.endpoint.unicode_string(),
endpoint=data.endpoint,
auth_header=data.auth_header,
name=data.name,
replace_none=replace_none)

View file

@ -58,6 +58,7 @@ def get_event_type(event_type: Union[schemas.EventType, schemas.PerformanceEvent
schemas.EventType.REQUEST: "REQUEST",
schemas.EventType.REQUEST_DETAILS: "REQUEST",
schemas.PerformanceEventType.FETCH_FAILED: "REQUEST",
schemas.GraphqlFilterType.GRAPHQL_NAME: "GRAPHQL",
schemas.EventType.STATE_ACTION: "STATEACTION",
schemas.EventType.ERROR: "ERROR",
schemas.PerformanceEventType.LOCATION_AVG_CPU_LOAD: 'PERFORMANCE',

View file

@ -1,4 +1,5 @@
#!/bin/sh
export TZ=UTC
sh env_vars.sh
source /tmp/.env.override

View file

@ -1,4 +1,5 @@
#!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore
sh env_vars.sh
source /tmp/.env.override

View file

@ -1,4 +1,5 @@
#!/bin/sh
export TZ=UTC
export ASSIST_KEY=ignore
sh env_vars.sh
source /tmp/.env.override

View file

@ -50,16 +50,19 @@ func NewStorage(cfg *config.ObjectsConfig) (objectstorage.ObjectStorage, error)
}, nil
}
func (s *storageImpl) Upload(reader io.Reader, key string, contentType string, compression objectstorage.CompressionType) error {
func (s *storageImpl) Upload(reader io.Reader, key string, contentType, contentEncoding string, compression objectstorage.CompressionType) error {
cacheControl := "max-age=2628000, immutable, private"
var contentEncoding *string
var encoding *string
switch compression {
case objectstorage.Gzip:
gzipStr := "gzip"
contentEncoding = &gzipStr
encoding = &gzipStr
case objectstorage.Brotli:
gzipStr := "br"
contentEncoding = &gzipStr
encoding = &gzipStr
}
if contentEncoding != "" {
encoding = &contentEncoding
}
// Remove leading slash to avoid empty folder creation
if strings.HasPrefix(key, "/") {
@ -68,7 +71,7 @@ func (s *storageImpl) Upload(reader io.Reader, key string, contentType string, c
_, err := s.client.UploadStream(context.Background(), s.container, key, reader, &azblob.UploadStreamOptions{
HTTPHeaders: &blob.HTTPHeaders{
BlobCacheControl: &cacheControl,
BlobContentEncoding: contentEncoding,
BlobContentEncoding: encoding,
BlobContentType: &contentType,
},
Tags: s.tags,

View file

@ -93,6 +93,15 @@ DROP TYPE IF EXISTS events.resource_method;
ALTER TYPE integration_provider ADD VALUE IF NOT EXISTS 'dynatrace';
UPDATE users SET settings=COALESCE(settings, '{}'::jsonb) || '{
"modules": [
"usability-tests",
"feature-flags"
]
}'::jsonb
WHERE settings IS NULL
OR settings -> 'modules' IS NULL;
COMMIT;
\elif :is_next

View file

@ -198,6 +198,11 @@ export default class APIClient {
}
return fetch(edp + _path, init).then((response) => {
if (response.status === 403) {
console.warn('API returned 403. Clearing JWT token.');
this.onUpdateJwt({ jwt: undefined }); // Clear the token
}
if (response.ok) {
return response;
} else {

View file

@ -25,6 +25,10 @@ function AssistSearchField() {
const showRecords = () => {
showModal(<Recordings />, { right: true, width: 960 });
};
const onClear = () => {
searchStoreLive.clearSearch();
}
return (
<div className="flex items-center w-full gap-2">
<div style={{ width: '60%' }}>
@ -42,7 +46,7 @@ function AssistSearchField() {
type="link"
className="ml-auto font-medium"
disabled={!hasFilters && !hasEvents}
onClick={() => searchStoreLive.clearSearch()}
onClick={onClear}
>
Clear Search
</Button>

View file

@ -2,8 +2,10 @@ import React from 'react';
import LiveSessionList from 'Shared/LiveSessionList';
import LiveSessionSearch from 'Shared/LiveSessionSearch';
import AssistSearchField from './AssistSearchField';
import usePageTitle from '@/hooks/usePageTitle';
function AssistView() {
usePageTitle('Co-Browse - OpenReplay');
return (
<div className="w-full mx-auto" style={{ maxWidth: '1360px'}}>
<AssistSearchField />

View file

@ -18,7 +18,7 @@ function UserForm() {
const isSaving = userStore.saving;
const user: any = userStore.instance || userStore.initUser();
const roles = roleStore.list
.filter((r) => (r.isProtected ? user.isSuperAdmin : true))
.filter((r) => (r.protected ? user.isSuperAdmin : true))
.map((r) => ({ label: r.name, value: r.roleId }));
const onChangeCheckbox = (e: any) => {

View file

@ -57,7 +57,7 @@ function CardSessionsByList({ list, selected, paginated, onClickHandler = () =>
<div className="m-0">
<div className="flex justify-between m-0 p-0">
<Typography.Text ellipsis={true}>{row.displayName}</Typography.Text>
<Typography.Text type="secondary"> {row.sessionCount}</Typography.Text>
<Typography.Text type="secondary" className="flex-shrink-0"> {row.sessionCount}</Typography.Text>
</div>
<Progress

View file

@ -7,6 +7,7 @@ import { ArrowRight } from 'lucide-react';
import CardSessionsByList from 'Components/Dashboard/Widgets/CardSessionsByList';
import { useModal } from 'Components/ModalContext';
import Widget from '@/mstore/types/widget';
import { FilterKey } from 'Types/filter/filterType';
interface Props {
metric?: any;
@ -18,7 +19,7 @@ interface Props {
function SessionsBy(props: Props) {
const { metric = {}, data = { values: [] }, onClick = () => null, isTemplate } = props;
const [selected, setSelected] = React.useState<any>(null);
const total = data.count;
const total = data.total;
const { openModal, closeModal } = useModal();
const modalMetric = React.useMemo(() => Object.assign(new Widget(), metric), [metric]);
@ -27,12 +28,22 @@ function SessionsBy(props: Props) {
...filtersMap[metric.metricOf],
value: [data.name],
type: filtersMap[metric.metricOf].key,
filters: filtersMap[metric.metricOf].filters?.map((f: any) => {
const { key, operatorOptions, category, icon, label, options, ...cleaned } = f;
return { ...cleaned, type: f.key, value: [] };
})
filters: [],
};
if (metric.metricOf === FilterKey.FETCH) {
baseFilter.filters = [
{
key: FilterKey.FETCH_URL,
operator: 'is',
value: [data.name],
type: FilterKey.FETCH_URL,
}
];
}
const { key, operatorOptions, category, icon, label, options, ...finalFilter } = baseFilter;
setSelected(data.name);

View file

@ -9,7 +9,6 @@ import {
import React from 'react';
import ExCard from './ExCard';
import { size } from '@floating-ui/react-dom-interactions';
const TYPES = {
Frustrations: 'frustrations',

View file

@ -6,7 +6,7 @@ import { CARD_LIST, CARD_CATEGORIES, CardType } from './ExampleCards';
import { useStore } from 'App/mstore';
import Option from './Option';
import CardsLibrary from 'Components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary';
import { FUNNEL } from 'App/constants/card';
import { FUNNEL, TABLE } from 'App/constants/card';
import { useHistory } from 'react-router';
import { FilterKey } from 'Types/filter/filterType';
import FilterSeries from '@/mstore/types/filterSeries';
@ -72,9 +72,18 @@ const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
name: "Series 1",
filter: {
filters: selectedCard.filters,
eventsOrder: selectedCard.cardType === TABLE ? 'and' : 'then'
}
})
];
} else if (selectedCard.cardType === TABLE) {
cardData.series = [new FilterSeries().fromJson({
name: "Series 1",
filter: {
filters: [],
eventsOrder: 'and'
}
})];
}
if (selectedCard.cardType === FUNNEL) {

View file

@ -41,6 +41,7 @@ function ExcludeFilters(props: Props) {
onRemoveFilter={() => onRemoveFilter(index)}
// saveRequestPayloads={saveRequestPayloads}
disableDelete={false}
allowedFilterKeys={[FilterKey.LOCATION, FilterKey.CLICK, FilterKey.INPUT, FilterKey.CUSTOM]}
// excludeFilterKeys={excludeFilterKeys}
/>
))}

View file

@ -136,9 +136,10 @@ function FilterSeries(props: Props) {
toggleExpand={() => setExpanded(!expanded)}/>
)}
{expandable && (
{expandable && !expanded && (
<Space className="justify-between w-full px-5 py-2 cursor-pointer" onClick={() => setExpanded(!expanded)}>
<div>{!expanded && <FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>}</div>
<FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>
{/*<div>{!expanded && <FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>}</div>*/}
<Button size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
@ -156,13 +157,13 @@ function FilterSeries(props: Props) {
supportsEmpty={supportsEmpty}
onFilterMove={onFilterMove}
excludeFilterKeys={excludeFilterKeys}
// actions={[
// expandable && (
// <Button onClick={() => setExpanded(!expanded)}
// size="small"
// icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
// )
// ]}
actions={[
expandable && (
<Button onClick={() => setExpanded(!expanded)}
size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
)
]}
/>
) : (
<div className="color-gray-medium">{emptyMessage}</div>

View file

@ -1,6 +1,5 @@
import { useStore } from 'App/mstore';
import React from 'react';
// import Select from 'Shared/Select';
import { Select } from 'antd';
const sortOptions = [

View file

@ -68,7 +68,7 @@ const FilterSection = observer(({ metric, excludeFilterKeys }: any) => {
const eventsLength = metric.series[0].filter.filters.filter((i: any) => i && i.isEvent).length;
// const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
const isSingleSeries = isTable || isFunnel || isClickMap || isInsights || isRetention;
const isSingleSeries = isTable || isFunnel || isClickMap || isInsights || isRetention || isPathAnalysis;
// const onAddFilter = (filter: any) => {
// metric.series[0].filter.addFilter(filter);

View file

@ -1,221 +1,240 @@
import React, { useEffect, useState } from 'react';
import { NoContent, Loader, Pagination, Button } from 'UI';
import React, {useEffect, useState} from 'react';
import {NoContent, Loader, Pagination, Button} from 'UI';
import Select from 'Shared/Select';
import cn from 'classnames';
import { useStore } from 'App/mstore';
import {useStore} from 'App/mstore';
import SessionItem from 'Shared/SessionItem';
import { observer } from 'mobx-react-lite';
import { DateTime } from 'luxon';
import { debounce } from 'App/utils';
import {observer} from 'mobx-react-lite';
import {DateTime} from 'luxon';
import {debounce} from 'App/utils';
import useIsMounted from 'App/hooks/useIsMounted';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { numberWithCommas } from 'App/utils';
import { HEATMAP } from 'App/constants/card';
import { Tag } from 'antd';
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
import {numberWithCommas} from 'App/utils';
import {HEATMAP} from 'App/constants/card';
import {Tag} from 'antd';
interface Props {
className?: string;
className?: string;
}
function WidgetSessions(props: Props) {
const { className = '' } = props;
const [activeSeries, setActiveSeries] = useState('all');
const [data, setData] = useState<any>([]);
const isMounted = useIsMounted();
const [loading, setLoading] = useState(false);
const filteredSessions = getListSessionsBySeries(data, activeSeries);
const { dashboardStore, metricStore, sessionStore, customFieldStore } = useStore();
const filter = dashboardStore.drillDownFilter;
const widget = metricStore.instance;
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm');
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm');
const [seriesOptions, setSeriesOptions] = useState([{ label: 'All', value: 'all' }]);
const hasFilters = filter.filters.length > 0 || (filter.startTimestamp !== dashboardStore.drillDownPeriod.start || filter.endTimestamp !== dashboardStore.drillDownPeriod.end);
const filterText = filter.filters.length > 0 ? filter.filters[0].value : '';
const metaList = customFieldStore.list.map((i: any) => i.key);
const {className = ''} = props;
const [activeSeries, setActiveSeries] = useState('all');
const [data, setData] = useState<any>([]);
const isMounted = useIsMounted();
const [loading, setLoading] = useState(false);
const filteredSessions = getListSessionsBySeries(data, activeSeries);
const {dashboardStore, metricStore, sessionStore, customFieldStore} = useStore();
const filter = dashboardStore.drillDownFilter;
const widget = metricStore.instance;
const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm');
const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm');
const [seriesOptions, setSeriesOptions] = useState([{label: 'All', value: 'all'}]);
const hasFilters = filter.filters.length > 0 || (filter.startTimestamp !== dashboardStore.drillDownPeriod.start || filter.endTimestamp !== dashboardStore.drillDownPeriod.end);
const filterText = filter.filters.length > 0 ? filter.filters[0].value : '';
const metaList = customFieldStore.list.map((i: any) => i.key);
const writeOption = ({ value }: any) => setActiveSeries(value.value);
useEffect(() => {
if (!data) return;
const seriesOptions = data.map((item: any) => ({
label: item.seriesName,
value: item.seriesId
}));
setSeriesOptions([{ label: 'All', value: 'all' }, ...seriesOptions]);
}, [data]);
const writeOption = ({value}: any) => setActiveSeries(value.value);
useEffect(() => {
if (!data) return;
const seriesOptions = data.map((item: any) => ({
label: item.seriesName,
value: item.seriesId
}));
setSeriesOptions([{label: 'All', value: 'all'}, ...seriesOptions]);
}, [data]);
const fetchSessions = (metricId: any, filter: any) => {
if (!isMounted()) return;
setLoading(true);
delete filter.eventsOrderSupport;
widget
.fetchSessions(metricId, filter)
.then((res: any) => {
setData(res);
})
.finally(() => {
setLoading(false);
});
};
const fetchClickmapSessions = (customFilters: Record<string, any>) => {
sessionStore.getSessions(customFilters).then((data) => {
setData([{ ...data, seriesId: 1, seriesName: 'Clicks' }]);
});
};
const debounceRequest: any = React.useCallback(debounce(fetchSessions, 1000), []);
const debounceClickMapSearch = React.useCallback(debounce(fetchClickmapSessions, 1000), []);
const fetchSessions = (metricId: any, filter: any) => {
if (!isMounted()) return;
setLoading(true);
const depsString = JSON.stringify(widget.series);
const filterCopy = {...filter};
delete filterCopy.eventsOrderSupport;
const loadData = () => {
if (widget.metricType === HEATMAP && metricStore.clickMapSearch) {
const clickFilter = {
value: [metricStore.clickMapSearch],
type: 'CLICK',
operator: 'onSelector',
isEvent: true,
// @ts-ignore
filters: []
};
const timeRange = {
rangeValue: dashboardStore.drillDownPeriod.rangeValue,
startDate: dashboardStore.drillDownPeriod.start,
endDate: dashboardStore.drillDownPeriod.end
};
const customFilter = {
...filter,
...timeRange,
filters: [...sessionStore.userFilter.filters, clickFilter]
};
debounceClickMapSearch(customFilter);
} else {
debounceRequest(widget.metricId, {
...filter,
series: widget.series.map((s) => s.toJson()),
page: metricStore.sessionsPage,
limit: metricStore.sessionsPageSize
});
}
};
useEffect(() => {
metricStore.updateKey('sessionsPage', 1);
loadData();
}, [
filter.startTimestamp,
filter.endTimestamp,
filter.filters,
depsString,
metricStore.clickMapSearch,
activeSeries
]);
useEffect(loadData, [metricStore.sessionsPage]);
const clearFilters = () => {
metricStore.updateKey('sessionsPage', 1);
dashboardStore.resetDrillDownFilter();
};
return (
<div className={cn(className, 'bg-white p-3 pb-0 rounded-lg shadow-sm border mt-3')}>
<div className="flex items-center justify-between">
<div>
<div className="flex items-baseline">
<h2 className="text-xl">{metricStore.clickMapSearch ? 'Clicks' : 'Sessions'}</h2>
<div className="ml-2 color-gray-medium">
{metricStore.clickMapLabel ? `on "${metricStore.clickMapLabel}" ` : null}
between <span className="font-medium color-gray-darkest">{startTime}</span> and{' '}
<span className="font-medium color-gray-darkest">{endTime}</span>{' '}
</div>
</div>
{hasFilters && widget.metricType === 'table' &&
<div className="py-2"><Tag closable onClose={clearFilters}>{filterText}</Tag></div>}
</div>
<div className="flex items-center gap-4">
{hasFilters && <Button variant="text-primary" onClick={clearFilters}>Clear Filters</Button>}
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Filter by Series</span>
<Select options={seriesOptions} defaultValue={'all'} onChange={writeOption} plain />
</div>
)}
</div>
</div>
<div className="mt-3">
<Loader loading={loading}>
<NoContent
title={
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_SESSIONS} size={60} />
<div className="mt-4" />
<div className="text-center">
No relevant sessions found for the selected time period
</div>
</div>
try {
// Handle filters properly with null checks
if (filterCopy.filters && filterCopy.filters.length > 0) {
// Ensure the nested path exists before pushing
if (filterCopy.series?.[0]?.filter) {
if (!filterCopy.series[0].filter.filters) {
filterCopy.series[0].filter.filters = [];
}
filterCopy.series[0].filter.filters.push(...filterCopy.filters);
}
filterCopy.filters = [];
}
show={filteredSessions.sessions.length === 0}
>
{filteredSessions.sessions.map((session: any) => (
<React.Fragment key={session.sessionId}>
<SessionItem session={session} metaList={metaList} />
<div className="border-b" />
</React.Fragment>
))}
} catch (e) {
// do nothing
}
widget
.fetchSessions(metricId, filterCopy)
.then((res: any) => {
setData(res);
})
.finally(() => {
setLoading(false);
});
};
const fetchClickmapSessions = (customFilters: Record<string, any>) => {
sessionStore.getSessions(customFilters).then((data) => {
setData([{...data, seriesId: 1, seriesName: 'Clicks'}]);
});
};
const debounceRequest: any = React.useCallback(debounce(fetchSessions, 1000), []);
const debounceClickMapSearch = React.useCallback(debounce(fetchClickmapSessions, 1000), []);
<div className="flex items-center justify-between p-5">
<div>
Showing{' '}
<span className="font-medium">
const depsString = JSON.stringify(widget.series);
const loadData = () => {
if (widget.metricType === HEATMAP && metricStore.clickMapSearch) {
const clickFilter = {
value: [metricStore.clickMapSearch],
type: 'CLICK',
operator: 'onSelector',
isEvent: true,
// @ts-ignore
filters: []
};
const timeRange = {
rangeValue: dashboardStore.drillDownPeriod.rangeValue,
startDate: dashboardStore.drillDownPeriod.start,
endDate: dashboardStore.drillDownPeriod.end
};
const customFilter = {
...filter,
...timeRange,
filters: [...sessionStore.userFilter.filters, clickFilter]
};
debounceClickMapSearch(customFilter);
} else {
debounceRequest(widget.metricId, {
...filter,
series: widget.series.map((s) => s.toJson()),
page: metricStore.sessionsPage,
limit: metricStore.sessionsPageSize
});
}
};
useEffect(() => {
metricStore.updateKey('sessionsPage', 1);
loadData();
}, [
filter.startTimestamp,
filter.endTimestamp,
filter.filters,
depsString,
metricStore.clickMapSearch,
activeSeries
]);
useEffect(loadData, [metricStore.sessionsPage]);
const clearFilters = () => {
metricStore.updateKey('sessionsPage', 1);
dashboardStore.resetDrillDownFilter();
};
return (
<div className={cn(className, 'bg-white p-3 pb-0 rounded-lg shadow-sm border mt-3')}>
<div className="flex items-center justify-between">
<div>
<div className="flex items-baseline">
<h2 className="text-xl">{metricStore.clickMapSearch ? 'Clicks' : 'Sessions'}</h2>
<div className="ml-2 color-gray-medium">
{metricStore.clickMapLabel ? `on "${metricStore.clickMapLabel}" ` : null}
between <span className="font-medium color-gray-darkest">{startTime}</span> and{' '}
<span className="font-medium color-gray-darkest">{endTime}</span>{' '}
</div>
</div>
{hasFilters && widget.metricType === 'table' &&
<div className="py-2"><Tag closable onClose={clearFilters}>{filterText}</Tag></div>}
</div>
<div className="flex items-center gap-4">
{hasFilters && <Button variant="text-primary" onClick={clearFilters}>Clear Filters</Button>}
{widget.metricType !== 'table' && widget.metricType !== HEATMAP && (
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Filter by Series</span>
<Select options={seriesOptions} defaultValue={'all'} onChange={writeOption} plain/>
</div>
)}
</div>
</div>
<div className="mt-3">
<Loader loading={loading}>
<NoContent
title={
<div className="flex items-center justify-center flex-col">
<AnimatedSVG name={ICONS.NO_SESSIONS} size={60}/>
<div className="mt-4"/>
<div className="text-center">
No relevant sessions found for the selected time period
</div>
</div>
}
show={filteredSessions.sessions.length === 0}
>
{filteredSessions.sessions.map((session: any) => (
<React.Fragment key={session.sessionId}>
<SessionItem session={session} metaList={metaList}/>
<div className="border-b"/>
</React.Fragment>
))}
<div className="flex items-center justify-between p-5">
<div>
Showing{' '}
<span className="font-medium">
{(metricStore.sessionsPage - 1) * metricStore.sessionsPageSize + 1}
</span>{' '}
to{' '}
<span className="font-medium">
to{' '}
<span className="font-medium">
{(metricStore.sessionsPage - 1) * metricStore.sessionsPageSize +
filteredSessions.sessions.length}
filteredSessions.sessions.length}
</span>{' '}
of <span className="font-medium">{numberWithCommas(filteredSessions.total)}</span>{' '}
sessions.
</div>
<Pagination
page={metricStore.sessionsPage}
total={filteredSessions.total}
onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)}
limit={metricStore.sessionsPageSize}
debounceRequest={500}
/>
of <span className="font-medium">{numberWithCommas(filteredSessions.total)}</span>{' '}
sessions.
</div>
<Pagination
page={metricStore.sessionsPage}
total={filteredSessions.total}
onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)}
limit={metricStore.sessionsPageSize}
debounceRequest={500}
/>
</div>
</NoContent>
</Loader>
</div>
</NoContent>
</Loader>
</div>
</div>
);
</div>
);
}
const getListSessionsBySeries = (data: any, seriesId: any) => {
const arr = data.reduce(
(arr: any, element: any) => {
if (seriesId === 'all') {
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
const sessions = element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId));
arr.sessions.push(...sessions);
} else if (element.seriesId === seriesId) {
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
const sessions = element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId));
const duplicates = element.sessions.length - sessions.length;
arr.sessions.push(...sessions);
arr.total = element.total - duplicates;
}
return arr;
},
{ sessions: [] }
);
arr.total =
seriesId === 'all'
? Math.max(...data.map((i: any) => i.total))
: data.find((i: any) => i.seriesId === seriesId).total;
return arr;
const arr = data.reduce(
(arr: any, element: any) => {
if (seriesId === 'all') {
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
const sessions = element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId));
arr.sessions.push(...sessions);
} else if (element.seriesId === seriesId) {
const sessionIds = arr.sessions.map((i: any) => i.sessionId);
const sessions = element.sessions.filter((i: any) => !sessionIds.includes(i.sessionId));
const duplicates = element.sessions.length - sessions.length;
arr.sessions.push(...sessions);
arr.total = element.total - duplicates;
}
return arr;
},
{sessions: []}
);
arr.total =
seriesId === 'all'
? Math.max(...data.map((i: any) => i.total))
: data.find((i: any) => i.seriesId === seriesId).total;
return arr;
};
export default observer(WidgetSessions);

View file

@ -1,6 +1,5 @@
import React from 'react';
import { numberWithCommas } from 'App/utils';
import withPageTitle from 'HOCs/withPageTitle';
import withPermissions from 'HOCs/withPermissions';
import FFlagsListHeader from 'Components/FFlags/FFlagsListHeader';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
@ -9,8 +8,10 @@ import FFlagItem from './FFlagItem';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import Select from 'Shared/Select';
import usePageTitle from '@/hooks/usePageTitle';
function FFlagsList({ siteId }: { siteId: string }) {
usePageTitle('Feature Flags - OpenReplay');
const { featureFlagsStore, userStore } = useStore();
React.useEffect(() => {
@ -31,7 +32,7 @@ function FFlagsList({ siteId }: { siteId: string }) {
options={[
{ label: 'All', value: '0' as const },
{ label: 'Enabled', value: '1' as const },
{ label: 'Disabled', value: '2' as const },
{ label: 'Disabled', value: '2' as const }
]}
defaultValue={featureFlagsStore.activity}
plain
@ -45,7 +46,7 @@ function FFlagsList({ siteId }: { siteId: string }) {
<Select
options={[
{ label: 'Newest', value: 'DESC' },
{ label: 'Oldest', value: 'ASC' },
{ label: 'Oldest', value: 'ASC' }
]}
defaultValue={featureFlagsStore.sort.order}
plain
@ -65,7 +66,7 @@ function FFlagsList({ siteId }: { siteId: string }) {
<AnimatedSVG name={ICONS.NO_FFLAGS} size={60} />
<div className="text-center mt-4 text-lg font-medium">
{featureFlagsStore.sort.query === ''
? "You haven't created any feature flags yet"
? 'You haven\'t created any feature flags yet'
: 'No matching results'}
</div>
</div>
@ -121,6 +122,4 @@ function FFlagsList({ siteId }: { siteId: string }) {
);
}
export default withPageTitle('Feature Flags')(
withPermissions(['FEATURE_FLAGS'])(observer(FFlagsList))
);
export default withPermissions(['FEATURE_FLAGS'])(observer(FFlagsList));

View file

@ -32,13 +32,7 @@ function CreatePassword(props: Props) {
if (!validatePassword(password)) {
return;
}
resetPassword({ invitation, pass, password }).then((response: any) => {
if (response && response.errors && response.errors.length > 0) {
setError(response.errors[0]);
} else {
setUpdated(true);
}
});
void resetPassword({ invitation, pass, password });
};
const onSubmit = (e: any) => {
@ -102,7 +96,7 @@ function CreatePassword(props: Props) {
/>
</Form.Field>
<Form.Field>
<label>{'Cofirm password'}</label>
<label>{'Confirm password'}</label>
<Input
autoComplete="new-password"
type="password"

View file

@ -1,163 +1,187 @@
import React, { useEffect } from 'react';
import Widget from 'App/mstore/types/widget';
import Funnelbar, { UxTFunnelBar } from "./FunnelBar";
import Funnelbar, { UxTFunnelBar } from './FunnelBar';
import cn from 'classnames';
import stl from './FunnelWidget.module.css';
import { observer } from 'mobx-react-lite';
import { NoContent, Icon } from 'UI';
import { Tag, Tooltip } from 'antd';
import { useModal } from 'App/components/Modal';
import { useStore } from '@/mstore';
import Filter from '@/mstore/types/filter';
interface Props {
metric?: Widget;
isWidget?: boolean;
data: any;
metric?: Widget;
isWidget?: boolean;
data: any;
}
function FunnelWidget(props: Props) {
const [focusedFilter, setFocusedFilter] = React.useState<number | null>(null);
const { isWidget = false, data, metric } = props;
const funnel = data.funnel || { stages: [] };
const totalSteps = funnel.stages.length;
const stages = isWidget ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] : funnel.stages;
const hasMoreSteps = funnel.stages.length > 2;
const lastStage = funnel.stages[funnel.stages.length - 1];
const remainingSteps = totalSteps - 2;
const { hideModal } = useModal();
const metricLabel = metric?.metricFormat == 'userCount' ? 'Users' : 'Sessions';
const { dashboardStore, searchStore } = useStore();
const [focusedFilter, setFocusedFilter] = React.useState<number | null>(null);
const { isWidget = false, data, metric } = props;
const funnel = data.funnel || { stages: [] };
const totalSteps = funnel.stages.length;
const stages = isWidget ? [...funnel.stages.slice(0, 1), funnel.stages[funnel.stages.length - 1]] : funnel.stages;
const hasMoreSteps = funnel.stages.length > 2;
const lastStage = funnel.stages[funnel.stages.length - 1];
const remainingSteps = totalSteps - 2;
const { hideModal } = useModal();
const metricLabel = metric?.metricFormat == 'userCount' ? 'Users' : 'Sessions';
const drillDownFilter = dashboardStore.drillDownFilter;
const drillDownPeriod = dashboardStore.drillDownPeriod;
const metricFilters = metric?.series[0]?.filter.filters || [];
useEffect(() => {
return () => {
if (isWidget) return;
hideModal();
const applyDrillDown = (index: number) => {
const filter = new Filter().fromData({ filters: metricFilters.slice(0, index + 1) });
const periodTimestamps = drillDownPeriod.toTimestamps();
drillDownFilter.merge({
filters: filter.toJson().filters,
startTimestamp: periodTimestamps.startTimestamp,
endTimestamp: periodTimestamps.endTimestamp
});
};
useEffect(() => {
return () => {
if (isWidget) return;
hideModal();
};
}, []);
const focusStage = (index: number) => {
funnel.stages.forEach((s, i) => {
// turning on all filters if one was focused already
if (focusedFilter === index) {
s.updateKey('isActive', true);
setFocusedFilter(null);
} else {
setFocusedFilter(index);
if (i === index) {
s.updateKey('isActive', true);
} else {
s.updateKey('isActive', false);
}
}, []);
}
});
const focusStage = (index: number) => {
funnel.stages.forEach((s, i) => {
// turning on all filters if one was focused already
if (focusedFilter === index) {
s.updateKey('isActive', true)
setFocusedFilter(null)
} else {
setFocusedFilter(index)
if (i === index) {
s.updateKey('isActive', true)
} else {
s.updateKey('isActive', false)
}
}
})
}
applyDrillDown(focusedFilter === index ? -1 : index);
};
return (
<NoContent
style={{ minHeight: 220 }}
title={
<div className="flex items-center text-lg">
<Icon name="info-circle" className="mr-2" size="18" />
No data available for the selected period.
</div>
}
show={!stages || stages.length === 0}
>
<div className="w-full">
{ !isWidget && (
stages.map((filter: any, index: any) => (
<Stage
key={index}
index={index + 1}
isWidget={isWidget}
stage={filter}
focusStage={focusStage}
focusedFilter={focusedFilter}
metricLabel={metricLabel}
/>
))
)}
return (
<NoContent
style={{ minHeight: 220 }}
title={
<div className="flex items-center text-lg">
<Icon name="info-circle" className="mr-2" size="18" />
No data available for the selected period.
</div>
}
show={!stages || stages.length === 0}
>
<div className="w-full">
{!isWidget && (
stages.map((filter: any, index: any) => (
<Stage
key={index}
index={index + 1}
isWidget={isWidget}
stage={filter}
focusStage={focusStage}
focusedFilter={focusedFilter}
metricLabel={metricLabel}
/>
))
)}
{ isWidget && (
<>
<Stage index={1} isWidget={isWidget} stage={stages[0]} />
{isWidget && (
<>
<Stage index={1} isWidget={isWidget} stage={stages[0]} />
{ hasMoreSteps && (
<>
<EmptyStage total={remainingSteps} />
</>
)}
{hasMoreSteps && (
<>
<EmptyStage total={remainingSteps} />
</>
)}
{funnel.stages.length > 1 && (
<Stage index={totalSteps} isWidget={isWidget} stage={lastStage} />
)}
</>
)}
</div>
<div className="flex items-center pb-4">
<div className="flex items-center">
<span className="text-base font-medium mr-2">Lost conversion</span>
<Tooltip title={`${funnel.lostConversions} Sessions ${funnel.lostConversionsPercentage}%`}>
<Tag bordered={false} color="red" className='text-lg font-medium rounded-lg'>
{funnel.lostConversions}
</Tag>
</Tooltip>
</div>
<div className="mx-3" />
<div className="flex items-center">
<span className="text-base font-medium mr-2">Total conversion</span>
<Tooltip title={`${funnel.totalConversions} Sessions ${funnel.totalConversionsPercentage}%`}>
<Tag bordered={false} color="green" className='text-lg font-medium rounded-lg'>
{funnel.totalConversions}
</Tag>
</Tooltip>
</div>
</div>
{funnel.totalDropDueToIssues > 0 && <div className="flex items-center mb-2"><Icon name="magic" /> <span className="ml-2">{funnel.totalDropDueToIssues} sessions dropped due to issues.</span></div>}
</NoContent>
);
{funnel.stages.length > 1 && (
<Stage index={totalSteps} isWidget={isWidget} stage={lastStage} />
)}
</>
)}
</div>
<div className="flex items-center pb-4">
<div className="flex items-center">
<span className="text-base font-medium mr-2">Lost conversion</span>
<Tooltip title={`${funnel.lostConversions} Sessions ${funnel.lostConversionsPercentage}%`}>
<Tag bordered={false} color="red" className="text-lg font-medium rounded-lg">
{funnel.lostConversions}
</Tag>
</Tooltip>
</div>
<div className="mx-3" />
<div className="flex items-center">
<span className="text-base font-medium mr-2">Total conversion</span>
<Tooltip title={`${funnel.totalConversions} Sessions ${funnel.totalConversionsPercentage}%`}>
<Tag bordered={false} color="green" className="text-lg font-medium rounded-lg">
{funnel.totalConversions}
</Tag>
</Tooltip>
</div>
</div>
{funnel.totalDropDueToIssues > 0 && <div className="flex items-center mb-2"><Icon name="magic" /> <span
className="ml-2">{funnel.totalDropDueToIssues} sessions dropped due to issues.</span></div>}
</NoContent>
);
}
export const EmptyStage = observer(({ total }: any) => {
return (
<div className={cn("flex items-center mb-4 pb-3", stl.step)}>
<IndexNumber index={0} />
<div className="w-fit px-2 border border-teal py-1 text-center justify-center bg-teal-lightest flex items-center rounded-full color-teal" style={{ width: '100px'}}>
{`+${total} ${total > 1 ? 'steps' : 'step'}`}
</div>
<div className="border-b w-full border-dashed"></div>
</div>
)
})
return (
<div className={cn('flex items-center mb-4 pb-3', stl.step)}>
<IndexNumber index={0} />
<div
className="w-fit px-2 border border-teal py-1 text-center justify-center bg-teal-lightest flex items-center rounded-full color-teal"
style={{ width: '100px' }}>
{`+${total} ${total > 1 ? 'steps' : 'step'}`}
</div>
<div className="border-b w-full border-dashed"></div>
</div>
);
});
export const Stage = observer(({ metricLabel, stage, index, isWidget, uxt, focusStage, focusedFilter }: any) => {
return stage ? (
<div
className={cn('flex items-start', stl.step, { [stl['step-disabled']]: !stage.isActive })}
>
<IndexNumber index={index} />
{!uxt ? <Funnelbar metricLabel={metricLabel} index={index} filter={stage} focusStage={focusStage} focusedFilter={focusedFilter} /> : <UxTFunnelBar filter={stage} />}
{/*{!isWidget && !uxt && <BarActions bar={stage} />}*/}
</div>
) : (
<></>
)
})
return stage ? (
<div
className={cn('flex items-start', stl.step, { [stl['step-disabled']]: !stage.isActive })}
>
<IndexNumber index={index} />
{!uxt ? <Funnelbar metricLabel={metricLabel} index={index} filter={stage} focusStage={focusStage}
focusedFilter={focusedFilter} /> : <UxTFunnelBar filter={stage} />}
{/*{!isWidget && !uxt && <BarActions bar={stage} />}*/}
</div>
) : (
<></>
);
});
export const IndexNumber = observer(({ index }: any) => {
return (
<div className="z-10 w-6 h-6 border shrink-0 mr-4 text-sm rounded-full bg-gray-lightest flex items-center justify-center leading-3">
{index === 0 ? <Icon size="14" color="gray-dark" name="list" /> : index}
</div>
);
})
return (
<div
className="z-10 w-6 h-6 border shrink-0 mr-4 text-sm rounded-full bg-gray-lightest flex items-center justify-center leading-3">
{index === 0 ? <Icon size="14" color="gray-dark" name="list" /> : index}
</div>
);
});
const BarActions = observer(({ bar }: any) => {
return (
<div className="self-end flex items-center justify-center ml-4" style={{ marginBottom: '49px'}}>
<button onClick={() => bar.updateKey('isActive', !bar.isActive)}>
<Icon name="eye-slash-fill" color={bar.isActive ? "gray-light" : "gray-darkest"} size="22" />
</button>
</div>
)
})
return (
<div className="self-end flex items-center justify-center ml-4" style={{ marginBottom: '49px' }}>
<button onClick={() => bar.updateKey('isActive', !bar.isActive)}>
<Icon name="eye-slash-fill" color={bar.isActive ? 'gray-light' : 'gray-darkest'} size="22" />
</button>
</div>
);
});
export default observer(FunnelWidget);

View file

@ -1,8 +1,5 @@
import React, { useEffect } from 'react';
import React from 'react';
import withPageTitle from 'HOCs/withPageTitle';
import NoSessionsMessage from 'Shared/NoSessionsMessage';
import MainSearchBar from 'Shared/MainSearchBar';
import SessionSearch from 'Shared/SessionSearch';
import SessionsTabOverview from 'Shared/SessionsTabOverview/SessionsTabOverview';
import FFlagsList from 'Components/FFlags';
import NewFFlag from 'Components/FFlags/NewFFlag';
@ -12,6 +9,8 @@ import { withRouter, RouteComponentProps, useLocation } from 'react-router-dom';
import FlagView from 'Components/FFlags/FlagView/FlagView';
import { observer } from 'mobx-react-lite';
import { useStore } from '@/mstore';
import NotesList from 'Shared/SessionsTabOverview/components/Notes/NoteList';
import Bookmarks from 'Shared/SessionsTabOverview/components/Bookmarks/Bookmarks';
// @ts-ignore
interface IProps extends RouteComponentProps {
@ -36,15 +35,22 @@ function Overview({ match: { params } }: IProps) {
return (
<Switch>
<Route exact strict
path={[withSiteId(sessions(), siteId), withSiteId(notes(), siteId), withSiteId(bookmarks(), siteId)]}>
path={withSiteId(sessions(), siteId)}>
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
<NoSessionsMessage siteId={siteId} />
<MainSearchBar />
<SessionSearch />
<div className="my-4" />
<SessionsTabOverview />
</div>
</Route>
<Route exact strict
path={withSiteId(bookmarks(), siteId)}>
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
<Bookmarks />
</div>
</Route>
<Route exact strict path={withSiteId(notes(), siteId)}>
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
<NotesList />
</div>
</Route>
<Route exact strict path={withSiteId(fflags(), siteId)}>
<FFlagsList siteId={siteId} />
</Route>

View file

@ -28,6 +28,7 @@ import {
import { useStore } from 'App/mstore';
import { session as sessionRoute, withSiteId } from 'App/routes';
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
import { MobEventsList, WebEventsList } from "../../../Session_/Player/Controls/EventsList";
import useShortcuts from '../ReplayPlayer/useShortcuts';
export const SKIP_INTERVALS = {

View file

@ -105,7 +105,7 @@ function PlayerBlockHeader(props: any) {
)}
</div>
</div>
<div className="relative border-l border-l-gray-lighter" style={{ minWidth: '270px' }}>
<div className="px-2 relative border-l border-l-gray-lighter" style={{ minWidth: '270px' }}>
<Tabs
tabs={TABS}
active={activeTab}

View file

@ -1,8 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { Segmented } from 'antd';
import React from 'react';
import { VList, VListHandle } from 'virtua';
import { PlayerContext } from "App/components/Session/playerContext";
import { PlayerContext } from 'App/components/Session/playerContext';
import { processLog, UnifiedLog } from './utils';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
@ -12,13 +11,12 @@ import {
} from 'App/components/Client/Integrations/apiMethods';
import BottomBlock from 'App/components/shared/DevTools/BottomBlock';
import { capitalize } from 'App/utils';
import { Icon, Input } from 'UI';
import { Icon } from 'UI';
import { Segmented, Input, Tooltip } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { client } from 'App/mstore';
import { FailedFetch, LoadingFetch } from "./StatusMessages";
import {
TableHeader,
LogRow
} from './Table'
import { FailedFetch, LoadingFetch } from './StatusMessages';
import { TableHeader, LogRow } from './Table';
async function fetchLogs(
tab: string,
@ -30,23 +28,24 @@ async function fetchLogs(
);
const json = await data.json();
try {
const logsResp = await fetch(json.url)
const logsResp = await fetch(json.url);
if (logsResp.ok) {
const logJson = await logsResp.json()
if (logJson.length === 0) return []
return processLog(logJson)
const logJson = await logsResp.json();
if (logJson.length === 0) return [];
return processLog(logJson);
} else {
throw new Error('Failed to fetch logs')
throw new Error('Failed to fetch logs');
}
} catch (e) {
console.log(e)
throw e
console.log(e);
throw e;
}
}
function BackendLogsPanel() {
const { projectsStore, sessionStore, integrationsStore } = useStore();
const integratedServices = integrationsStore.integrations.backendLogIntegrations;
const integratedServices =
integrationsStore.integrations.backendLogIntegrations;
const defaultTab = integratedServices[0]!.name;
const sessionId = sessionStore.currentId;
const projectId = projectsStore.siteId!;
@ -82,40 +81,59 @@ function BackendLogsPanel() {
return (
<BottomBlock style={{ height: '100%' }}>
<BottomBlock.Header>
<div className={'flex gap-2 items-center w-full'}>
<div className={'font-semibold'}>Traces</div>
{tabs.length && tab ? (
<div>
<Segmented options={tabs} value={tab} onChange={setTab} />
</div>
) : null}
<div className="flex items-center justify-between w-full">
<div className={'flex gap-2 items-center'}>
<div className={'font-semibold'}>Traces</div>
{tabs.length && tab ? (
<div>
<Segmented
options={tabs}
value={tab}
onChange={setTab}
size="small"
/>
</div>
) : null}
</div>
<div className={'ml-auto'} />
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
/>
<div className="flex items-center gap-2">
<Segmented
options={[
{ label: 'All Tabs', value: 'all' },
{
label: (
<Tooltip title="Backend logs are fetched for all tabs combined.">
<span>Current Tab</span>
</Tooltip>
),
value: 'current',
disabled: true,
},
]}
defaultValue="all"
size="small"
className="rounded-full font-medium"
/>
<Input
className="rounded-lg"
placeholder="Filter by keyword"
name="filter"
onChange={onFilterChange}
value={filter}
size="small"
prefix={<SearchOutlined className="text-neutral-400" />}
/>
</div>
</div>
</BottomBlock.Header>
<BottomBlock.Content className="overflow-y-auto">
{isPending ? (
<LoadingFetch provider={capitalize(tab)} />
) : null}
{isPending ? <LoadingFetch provider={capitalize(tab)} /> : null}
{isError ? (
<FailedFetch
provider={capitalize(tab)}
onRetry={refetch}
/>
) : null}
{isSuccess ? (
<LogsTable data={data} />
<FailedFetch provider={capitalize(tab)} onRetry={refetch} />
) : null}
{isSuccess ? <LogsTable data={data} /> : null}
</BottomBlock.Content>
</BottomBlock>
);
@ -128,8 +146,10 @@ const LogsTable = observer(({ data }: { data: UnifiedLog[] }) => {
const _list = React.useRef<VListHandle>(null);
const activeIndex = React.useMemo(() => {
const currTs = time + sessionStart;
const index = data.findIndex(
(log) => log.timestamp !== 'N/A' ? new Date(log.timestamp).getTime() >= currTs : false
const index = data.findIndex((log) =>
log.timestamp !== 'N/A'
? new Date(log.timestamp).getTime() >= currTs
: false
);
return index === -1 ? data.length - 1 : index;
}, [time, data.length]);
@ -141,17 +161,22 @@ const LogsTable = observer(({ data }: { data: UnifiedLog[] }) => {
const onJump = (ts: number) => {
player.jump(ts - sessionStart);
}
};
return (
<>
<TableHeader size={data.length} />
<VList ref={_list} count={data.length}>
{data.map((log, index) => (
<LogRow key={index} isActive={index === activeIndex} log={log} onJump={onJump} />
<LogRow
key={index}
isActive={index === activeIndex}
log={log}
onJump={onJump}
/>
))}
</VList>
</>
)
);
});
export default observer(BackendLogsPanel);

View file

@ -12,7 +12,7 @@ export function LoadingFetch({ provider }: { provider: string }) {
'w-full h-full flex items-center justify-center flex-col gap-2'
}
>
<LoadingOutlined style={{ fontSize: 32 }} />
<LoadingOutlined size={32} />
<div>Fetching logs from {provider}...</div>
</div>
);
@ -33,16 +33,23 @@ export function FailedFetch({
'w-full h-full flex flex-col items-center justify-center gap-2'
}
>
<Icon name={'exclamation-circle'} size={32} />
<div className={'flex items-center gap-1'}>
<div className={'flex items-center gap-1 font-medium'}>
<Icon name={'exclamation-circle'} size={14} />
<span>Failed to fetch logs from {provider}. </span>
<div className={'link'} onClick={onRetry}>
</div>
<div className='flex items-center gap-3'>
<Button type='text' size='small' onClick={onRetry}>
Retry
</div>
</div>
<div className={'link'} onClick={() => history.push(intPath)}>
</Button>
<Button type='text' size='small' onClick={() => history.push(intPath)}>
Check Configuration
</Button>
</div>
</div>
);
}

View file

@ -15,7 +15,7 @@ export function processLog(log: any): UnifiedLog[] {
} else if (isDynatraceLog(log)) {
return log.map(processDynatraceLog);
} else {
throw new Error("Unknown log format");
console.error("Unknown log format");
}
}
@ -37,7 +37,7 @@ function isElasticLog(log: any): boolean {
}
function isSentryLog(log: any): boolean {
return log && log[0].id && log[0].message && log[0].title;
return log && 'id' in log[0] && 'message' in log[0] && 'title' in log[0];
}
function processDynatraceLog(log: any): UnifiedLog {

View file

@ -2,14 +2,51 @@ import { useStore } from 'App/mstore';
import SaveModal from 'Components/Session/Player/TagWatch/SaveModal';
import React from 'react';
import { PlayerContext } from 'Components/Session/playerContext';
import { Button, Input } from 'antd';
import { CopyButton } from 'UI';
import { SearchOutlined, ZoomInOutlined } from '@ant-design/icons';
import { Button, Input, Tooltip } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import { ZoomInOutlined } from '@ant-design/icons';
import { observer } from 'mobx-react-lite';
import { useModal } from 'App/components/Modal';
import { toast } from 'react-toastify';
import { FilterKey } from "App/types/filter/filterType";
import { addOptionsToFilter } from "App/types/filter/newFilter";
import { FilterKey } from 'App/types/filter/filterType';
import { addOptionsToFilter } from 'App/types/filter/newFilter';
interface CopyableTextAreaProps {
selector: string;
setSelector: (value: string) => void;
}
const CopyableTextArea: React.FC<CopyableTextAreaProps> = ({ selector, setSelector }) => {
const handleCopy = () => {
navigator.clipboard.writeText(selector);
};
return (
<div className="w-full relative">
<Input.TextArea
value={selector}
onChange={(e) => setSelector(e.target.value)}
className="rounded-lg font-mono text-sm placeholder:font-sans placeholder:text-base placeholder:text-gray-400"
rows={4}
style={{ paddingRight: '40px' }}
placeholder='Enter selector to tag elements. E.g. .btn-primary'
/>
<Tooltip title="Copy">
<Button
type="text"
icon={<CopyOutlined />}
onClick={handleCopy}
style={{
position: 'absolute',
top: '8px',
right: '8px',
zIndex: 1,
}}
/>
</Tooltip>
</div>
);
};
function TagWatch() {
const { tagWatchStore, searchStore } = useStore();
@ -50,7 +87,7 @@ function TagWatch() {
ignoreClickRage: ignoreClRage,
ignoreDeadClick: ignoreDeadCl,
});
const tags = await tagWatchStore.getTags()
const tags = await tagWatchStore.getTags();
if (tags) {
addOptionsToFilter(
FilterKey.TAGGED_ELEMENT,
@ -58,42 +95,41 @@ function TagWatch() {
);
searchStore.refreshFilterOptions();
}
// @ts-ignore
toast.success('Tag created');
setSelector('');
return tag
return tag;
} catch {
// @ts-ignore
toast.error('Failed to create tag');
}
};
const openSaveModal = () => {
if (selector === '') {
return;
}
showModal(<SaveModal onSave={onSave} hideModal={hideModal} />, { right: true, width: 400 });
};
return (
<div className={'w-full h-full p-2 flex flex-col gap-2'}>
<div className={'flex items-center justify-between'}>
<div className={'font-semibold text-xl'}>Element Selector</div>
<CopyButton content={selector} />
<div className="w-full h-full p-4 flex flex-col gap-2">
<div className="flex flex-col items-center justify-between">
<p>Select elements in the session play area to tag by class selector and filter sessions to verify their rendering.</p>
</div>
<Input.TextArea value={selector} onChange={(e) => setSelector(e.target.value)} />
<CopyableTextArea selector={selector} setSelector={setSelector} />
<Button
onClick={openSaveModal}
type={'primary'}
type="primary"
ghost
icon={<ZoomInOutlined />}
disabled={selector === ''}
>
Tag Element
</Button>
<div className={'text-disabled-text text-sm'}>
Create and filter sessions by watch elements to determine if they rendered or not.
</div>
</div>
);
}
export default observer(TagWatch);
export default observer(TagWatch);

View file

@ -18,7 +18,7 @@ function RightBlock({
switch (activeTab) {
case 'EVENTS':
return (
<div className={cn('flex flex-col bg-white border-l', stl.panel)}>
<div className={cn('flex flex-col border-l', stl.panel)}>
<EventsBlock setActiveTab={setActiveTab} />
</div>
);

View file

@ -22,6 +22,7 @@ const Tabs = ({ tabs, active, onClick, border = true, className }: Props) => {
return (
<div className={cn(stl.tabs, className, { [stl.bordered]: border })} role="tablist">
<Segmented
size="small"
value={active}
options={tabs.map(({ key, text, hidden = false, disabled = false, iconComp = null }) => ({
label: (
@ -29,14 +30,14 @@ const Tabs = ({ tabs, active, onClick, border = true, className }: Props) => {
onClick={() => {
onClick(key);
}}
className={'font-semibold flex gap-1 items-center'}
className={'font-medium flex gap-1 items-center hover:text-teal rounded-lg'}
>
{iconComp ? iconComp : <Icon size={16} color={'black'} name={iconMap[key as keyof typeof iconMap]} />}
{iconComp ? iconComp : <Icon size={14} color="currentColor" style={{ fill: 'currentColor', strokeWidth:'0' }} name={iconMap[key as keyof typeof iconMap]} />}
<span>{text}</span>
</div>
),
value: key,
disabled: disabled,
disabled: disabled,
}))}
/>
</div>

View file

@ -159,7 +159,7 @@ const Event: React.FC<Props> = ({
>
<div className={cn(cls.main, 'flex flex-col w-full')}>
<div
className={cn('flex items-center w-full', { 'px-4': isLocation })}
className={cn('flex items-start w-full', { 'px-4': isLocation })}
>
<div style={{ minWidth: '16px' }}>
{event.type && iconName ? (
@ -169,20 +169,18 @@ const Event: React.FC<Props> = ({
)}
</div>
<div className="ml-3 w-full">
<div className="flex w-full items-first justify-between">
<div className="flex w-full items-start">
<div
className="flex items-center w-full"
className="flex flex-col justify-center items-start w-full"
style={{ minWidth: '0' }}
>
<span
className={cn(cls.title, { 'font-medium': isLocation })}
>
<span className={cn(cls.title, 'font-medium')}>
{title}
</span>
{body && !isLocation && (
<TextEllipsis
maxWidth="60%"
className="w-full ml-2 text-sm color-gray-medium"
maxWidth="80%"
className="w-full text-sm color-gray-medium"
text={body}
/>
)}
@ -202,8 +200,7 @@ const Event: React.FC<Props> = ({
{isLocation && (
<div className="pt-1 px-4">
<TextEllipsis
maxWidth="80%"
className="text-sm font-normal color-gray-medium"
className="text-sm ms-8 font-normal color-gray-medium"
text={body}
/>
</div>

View file

@ -8,6 +8,7 @@ import { Icon, TextEllipsis } from 'UI';
import Event from './Event';
import NoteEvent from './NoteEvent';
import stl from './eventGroupWrapper.module.css';
import cn from 'classnames'
function EventGroupWrapper(props) {
const { userStore } = useStore();
@ -132,7 +133,7 @@ function EventGroupWrapper(props) {
{isFirst && isLocation && event.referrer && (
<TextEllipsis>
<div className={stl.referrer}>
Referrer: <span className={stl.url}>{safeRef}</span>
Referrer: <span className={cn(stl.url, '!font-normal')}>{safeRef}</span>
</div>
</TextEllipsis>
)}

View file

@ -1,11 +1,12 @@
import React from 'react';
import { Input, Button } from 'UI';
import {Input, Button, Tooltip} from 'antd';
import {CloseOutlined, SearchOutlined} from '@ant-design/icons';
import { PlayerContext } from 'App/components/Session/playerContext';
function EventSearch(props) {
const { player } = React.useContext(PlayerContext);
const { onChange, value, header, setActiveTab } = props;
const { onChange, value, header, setActiveTab, eventsText } = props;
const toggleEvents = () => player.toggleEvents();
@ -16,25 +17,25 @@ function EventSearch(props) {
<Input
autoFocus
type="text"
placeholder="Filter"
className="inset-0 w-full"
placeholder={`Filter ${eventsText}`}
className="w-full rounded-lg"
name="query"
value={value}
onChange={onChange}
wrapperClassName="w-full"
style={{ height: '32px' }}
autoComplete="off chromebugfix"
prefix={<SearchOutlined />}
/>
<Button
className="ml-2"
icon="close"
variant="text"
onClick={() => {
setActiveTab('');
toggleEvents();
}}
/>
<Tooltip title="Close Panel" placement='bottom' >
<Button
className="ml-2"
type='text'
onClick={() => {
setActiveTab('');
toggleEvents();
}}
icon={<CloseOutlined />}
/>
</Tooltip>
</div>
</div>
</div>

View file

@ -196,7 +196,7 @@ function EventsBlock(props: IProps) {
return (
<>
<div className={cn(styles.header, 'p-4')}>
<div className={cn(styles.header, 'py-4 px-2 bg-gradient-to-t from-transparent to-neutral-50 h-[57px]' )}>
{uxtestingStore.isUxt() ? (
<div style={{ width: 240, height: 130 }} className={'relative'}>
<video
@ -219,14 +219,14 @@ function EventsBlock(props: IProps) {
</div>
</div>
) : null}
<div className={cn(styles.hAndProgress, 'mt-3')}>
<div className={cn(styles.hAndProgress, 'mt-0')}>
<EventSearch
onChange={write}
setActiveTab={setActiveTab}
value={query}
eventsText={usedEvents.length ? `${usedEvents.length} Events` : '0 Events'}
/>
</div>
<div className="mt-1 color-gray-medium">{eventsText}</div>
</div>
<div
className={cn('flex-1 pb-4', styles.eventsList)}

View file

@ -1,4 +1,5 @@
import { Segmented } from 'antd';
import {InfoCircleOutlined} from '@ant-design/icons'
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import React, { useEffect } from 'react';
@ -12,6 +13,7 @@ import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock';
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
import TimelineZoomButton from 'Components/Session_/Player/Controls/components/TimelineZoomButton';
import { Icon, NoContent } from 'UI';
import TabSelector from "../../shared/DevTools/TabSelector";
import BottomBlock from '../BottomBlock';
import EventRow from './components/EventRow';
@ -136,14 +138,60 @@ function WebOverviewPanelCont() {
const { endTime, currentTab, tabStates } = store.get();
const stackEventList = tabStates[currentTab]?.stackList || [];
const frustrationsList = tabStates[currentTab]?.frustrationsList || [];
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
const resourceListUnmap = tabStates[currentTab]?.resourceList || [];
const fetchList = tabStates[currentTab]?.fetchList || [];
const graphqlList = tabStates[currentTab]?.graphqlList || [];
const performanceChartData =
tabStates[currentTab]?.performanceChartData || [];
const tabValues = Object.values(tabStates);
const dataSource = uiPlayerStore.dataSource;
const showSingleTab = dataSource === 'current';
const {
stackEventList = [],
frustrationsList = [],
exceptionsList = [],
resourceListUnmap = [],
fetchList = [],
graphqlList = [],
performanceChartData = [],
} = React.useMemo(() => {
if (showSingleTab) {
const stackEventList = tabStates[currentTab].stackList;
const frustrationsList = tabStates[currentTab].frustrationsList;
const exceptionsList = tabStates[currentTab].exceptionsList;
const resourceListUnmap = tabStates[currentTab].resourceList;
const fetchList = tabStates[currentTab].fetchList;
const graphqlList = tabStates[currentTab].graphqlList;
const performanceChartData =
tabStates[currentTab].performanceChartData;
return {
stackEventList,
frustrationsList,
exceptionsList,
resourceListUnmap,
fetchList,
graphqlList,
performanceChartData,
}
} else {
const stackEventList = tabValues.flatMap((tab) => tab.stackList);
// these two are global
const frustrationsList = tabValues[0].frustrationsList;
const exceptionsList = tabValues[0].exceptionsList;
// we can't compute global chart data because some tabs coexist
const performanceChartData: any = [];
const resourceListUnmap = tabValues.flatMap((tab) => tab.resourceList);
const fetchList = tabValues.flatMap((tab) => tab.fetchList);
const graphqlList = tabValues.flatMap((tab) => tab.graphqlList);
return {
stackEventList,
frustrationsList,
exceptionsList,
resourceListUnmap,
fetchList,
graphqlList,
performanceChartData,
}
}
}, [tabStates, currentTab, dataSource, tabValues]);
const fetchPresented = fetchList.length > 0;
const resourceList = resourceListUnmap
@ -168,7 +216,18 @@ function WebOverviewPanelCont() {
PERFORMANCE: checkInZoomRange(performanceChartData),
FRUSTRATIONS: checkInZoomRange(frustrationsList),
};
}, [tabStates, currentTab, zoomEnabled, zoomStartTs, zoomEndTs]);
}, [
tabStates,
currentTab,
zoomEnabled,
zoomStartTs,
zoomEndTs,
resourceList.length,
exceptionsList.length,
stackEventList.length,
performanceChartData.length,
frustrationsList.length,
]);
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
@ -187,6 +246,7 @@ function WebOverviewPanelCont() {
sessionId={sessionId}
setZoomTab={setZoomTab}
zoomTab={zoomTab}
showSingleTab={showSingleTab}
/>
);
}
@ -238,6 +298,7 @@ function PanelComponent({
spotTime,
spotEndTime,
onClose,
showSingleTab,
}: any) {
return (
<React.Fragment>
@ -280,12 +341,13 @@ function PanelComponent({
) : null}
</div>
{isSpot ? null : (
<div className="flex items-center h-20 mr-4 gap-2">
<TimelineZoomButton />
<div className="flex items-center h-20 mr-4 gap-3">
<FeatureSelection
list={selectedFeatures}
updateList={setSelectedFeatures}
/>
{!isMobile ? <TabSelector /> : null}
<TimelineZoomButton />
</div>
)}
</BottomBlock.Header>
@ -302,12 +364,19 @@ function PanelComponent({
style={{ height: '60px', minHeight: 'unset', padding: 0 }}
title={
<div className="flex items-center">
<Icon name="info-circle" className="mr-2" size="18" />
<InfoCircleOutlined size={18} />
Select a debug option to visualize on timeline.
</div>
}
>
{isSpot ? <VerticalPointerLineComp time={spotTime} endTime={spotEndTime} /> : <VerticalPointerLine />}
{isSpot ? (
<VerticalPointerLineComp
time={spotTime}
endTime={spotEndTime}
/>
) : (
<VerticalPointerLine />
)}
{selectedFeatures.map((feature: any, index: number) => (
<div
key={feature}
@ -318,6 +387,7 @@ function PanelComponent({
<EventRow
isGraph={feature === 'PERFORMANCE'}
title={feature}
disabled={!isMobile && !showSingleTab}
list={resources[feature]}
renderElement={(pointer: any[], isGrouped: boolean) => (
<TimelinePointer

View file

@ -1,7 +1,9 @@
import React from 'react';
import cn from 'classnames';
import { getTimelinePosition } from 'App/utils';
import { Icon, Tooltip } from 'UI';
import { Icon } from 'UI';
import { InfoCircleOutlined} from '@ant-design/icons'
import {Tooltip} from 'antd';
import PerformanceGraph from '../PerformanceGraph';
interface Props {
list?: any[];
@ -13,9 +15,10 @@ interface Props {
isGraph?: boolean;
zIndex?: number;
noMargin?: boolean;
disabled?: boolean;
}
const EventRow = React.memo((props: Props) => {
const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props;
const { title, className, list = [], endTime = 0, isGraph = false, message = '', disabled } = props;
const scale = 100 / endTime;
const _list =
isGraph ? [] :
@ -82,7 +85,7 @@ const EventRow = React.memo((props: Props) => {
}
return groupedItems;
}, [list]);
}, [list.length]);
return (
<div
@ -91,21 +94,24 @@ const EventRow = React.memo((props: Props) => {
>
<div
className={cn(
'uppercase text-sm flex items-center py-1',
'uppercase text-sm flex items-center py-1 gap-1',
props.noMargin ? '' : 'ml-2'
)}
>
<div
style={{ zIndex: props.zIndex ? props.zIndex : undefined }}
className="mr-2 leading-none"
className="leading-none mt-0.5"
>
{title}
</div>
{message ? <RowInfo message={message} /> : null}
<Tooltip title={message} placement='left'>
<InfoCircleOutlined className='text-neutral-400' />
</Tooltip>
</div>
<div className="relative w-full" style={{ zIndex: props.zIndex ? props.zIndex : undefined }}>
{isGraph ? (
<PerformanceGraph list={list} />
<PerformanceGraph disabled={disabled} list={list} />
) : _list.length > 0 ? (
_list.map((item: { items: any[], left: number, isGrouped: boolean }, index: number) => {
const left = item.left
@ -123,7 +129,7 @@ const EventRow = React.memo((props: Props) => {
);
})
) : (
<div className={cn('color-gray-medium text-sm', props.noMargin ? '' : 'ml-4')}>
<div className={cn('color-gray-medium text-xs', props.noMargin ? '' : 'ml-2')}>
None captured.
</div>
)}
@ -133,11 +139,3 @@ const EventRow = React.memo((props: Props) => {
});
export default EventRow;
function RowInfo({ message }: any) {
return (
<Tooltip title={message} delay={0}>
<Icon name="info-circle" color="gray-medium" />
</Tooltip>
);
}

View file

@ -1,6 +1,8 @@
import React from 'react';
import { Popover, Checkbox } from 'antd';
import { Popover, Checkbox, Button } from 'antd';
import {EyeInvisibleOutlined} from '@ant-design/icons';
import { Icon } from 'UI'
import Funnel from '@/types/funnel';
const NETWORK = 'NETWORK';
const ERRORS = 'ERRORS';
@ -59,7 +61,7 @@ function FeatureSelection(props: Props) {
<Popover
trigger="click"
content={
<div>
<div className='flex flex-col gap-3'>
<div
className={'flex items-center gap-2 cursor-pointer'}
onClick={() => toggleAllFeatures()}
@ -81,10 +83,9 @@ function FeatureSelection(props: Props) {
</div>
}
>
<div className={'font-semibold flex items-center gap-2 text-main cursor-pointer'}>
<Icon size={16} name={'funnel'} color={'main'} />
<div>X-Ray Events</div>
</div>
<Button color='primary' size='small' type='text' className={'font-medium'} icon={<EyeInvisibleOutlined size={12} />} >
Hide / Show
</Button>
</Popover>
</React.Fragment>
);

View file

@ -1,82 +1,107 @@
import React from 'react';
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
import {InfoCircleOutlined} from '@ant-design/icons'
interface Props {
list: any;
list: any;
disabled?: boolean;
}
const PerformanceGraph = React.memo((props: Props) => {
const { list } = props;
const { list, disabled } = props;
const finalValues = React.useMemo(() => {
const cpuMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.cpu);
}, 0);
const cpuMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.cpu);
}, Infinity);
const finalValues = React.useMemo(() => {
const cpuMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.cpu);
}, 0);
const cpuMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.cpu);
}, Infinity);
const memoryMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.usedHeap);
}, Infinity);
const memoryMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.usedHeap);
}, 0);
const memoryMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.usedHeap);
}, Infinity);
const memoryMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.usedHeap);
}, 0);
const convertToPercentage = (val: number, max: number, min: number) => {
return ((val - min) / (max - min)) * 100;
};
const cpuValues = list.map((item: any) => convertToPercentage(item.cpu, cpuMax, cpuMin));
const memoryValues = list.map((item: any) => convertToPercentage(item.usedHeap, memoryMax, memoryMin));
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
const maxLength = Math.max(arr1.length, arr2.length);
const result = [];
for (let i = 0; i < maxLength; i++) {
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
result.push(num > 60 ? num : 1);
}
return result;
};
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
return finalValues;
}, []);
const data = list.map((item: any, index: number) => {
return {
time: item.time,
cpu: finalValues[index],
};
});
return (
<ResponsiveContainer height={35}>
<AreaChart
data={data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<linearGradient id="cpuGradientTimeline" x1="0" y1="0" x2="0" y2="1">
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
</linearGradient>
</defs>
{/* <Tooltip filterNull={false} /> */}
<Area
dataKey="cpu"
baseValue={5}
type="monotone"
stroke="none"
activeDot={false}
fill="url(#cpuGradientTimeline)"
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
const convertToPercentage = (val: number, max: number, min: number) => {
return ((val - min) / (max - min)) * 100;
};
const cpuValues = list.map((item: any) =>
convertToPercentage(item.cpu, cpuMax, cpuMin)
);
const memoryValues = list.map((item: any) =>
convertToPercentage(item.usedHeap, memoryMax, memoryMin)
);
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
const maxLength = Math.max(arr1.length, arr2.length);
const result = [];
for (let i = 0; i < maxLength; i++) {
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
result.push(num > 60 ? num : 1);
}
return result;
};
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
return finalValues;
}, [list.length]);
const data = list.map((item: any, index: number) => {
return {
time: item.time,
cpu: finalValues[index],
};
});
return (
<div className={'relative'}>
{disabled ? (
<div
className={
'flex justify-center'
}
>
<div className={'text-xs text-neutral-400 ps-2'}>
Multi-tab performance overview is not available.
</div>
</div>
) : null}
<ResponsiveContainer height={35}>
<AreaChart
data={data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<linearGradient
id="cpuGradientTimeline"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
</linearGradient>
</defs>
{/* <Tooltip filterNull={false} /> */}
<Area
dataKey="cpu"
baseValue={5}
type="monotone"
stroke="none"
activeDot={false}
fill="url(#cpuGradientTimeline)"
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
});
export default PerformanceGraph;

View file

@ -71,7 +71,7 @@ export function FrustrationElement({ item, createEventClickHandler }: CommonProp
const elData = getFrustration(item);
return (
<Tooltip
placement={'right'}
placement={'top'}
title={
<div className="">
<b>{elData.name}</b>

View file

@ -168,7 +168,7 @@ function GroupedIssue({
<div
onClick={onClick}
className={
'h-5 w-5 cursor-pointer rounded-full bg-red text-white font-bold flex items-center justify-center text-sm'
'h-5 w-5 cursor-pointer rounded-full bg-red text-white font-bold flex items-center justify-center text-xs'
}
>
{items.length}

View file

@ -1,12 +1,14 @@
import React, { useEffect, useState } from 'react';
import { Loader, Icon } from 'UI';
import { Loader } from 'UI';
import {Button, Tooltip} from 'antd';
import {CloseOutlined} from '@ant-design/icons';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import SelectorsList from './components/SelectorsList/SelectorsList';
import { PlayerContext } from 'App/components/Session/playerContext';
import { compareJsonObjects } from 'App/utils';
import Select from 'Shared/Select';
import {Select, Form} from 'antd';
const JUMP_OFFSET = 1000;
interface Props {
@ -58,34 +60,29 @@ function PageInsightsPanel({ setActiveTab }: Props) {
};
return (
<div className="p-4 bg-white">
<div className="pb-3 flex items-center" style={{ maxWidth: '241px', paddingTop: '5px' }}>
<div className="flex items-center">
<span className="mr-1 text-xl">Clicks</span>
</div>
<div
onClick={() => {
setActiveTab('');
}}
className="ml-auto flex items-center justify-center bg-white cursor-pointer"
>
<Icon name="close" size="18" />
</div>
</div>
<div className="mb-4 flex items-center">
<div className="mr-2 flex-shrink-0">In Page</div>
<Select
isSearchable={true}
right
placeholder="change"
options={urlOptions}
name="url"
defaultValue={defaultValue}
onChange={onPageSelect}
id="change-dropdown"
className="w-full"
style={{ width: '100%' }}
<div className="p-2 py-4 bg-white">
<div className="flex items-center gap-2 mb-3 overflow-hidden">
<div className="flex-shrink-0 font-medium">Page</div>
<Form.Item name="url" className='mb-0 w-[176px]'>
<Select
showSearch
placeholder="change"
options={urlOptions}
defaultValue={defaultValue}
onChange={onPageSelect}
id="change-dropdown"
className="w-full rounded-lg max-w-[270px]"
dropdownStyle={{ }}
/>
</Form.Item>
<Tooltip title="Close Panel" placement='bottomRight'>
<Button
className="ml-2"
type='text'
onClick={() => { setActiveTab(''); }}
icon={<CloseOutlined />}
/>
</Tooltip>
</div>
<Loader loading={loading}>
<SelectorsList />

View file

@ -1,7 +1,5 @@
.wrapper {
padding: 10px;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2);
border-radius: 3px;
padding: 1rem;
background-color: $gray-lightest;
margin-bottom: 15px;
@ -18,8 +16,6 @@
border-radius: 10px;
background-color: $tealx;
flex-shrink: 0;
border: solid thin white;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;

View file

@ -17,20 +17,20 @@ export default function SelectorCard({ index = 1, target, showContent }: Props)
return (
// @ts-ignore TODO for Alex
<div className={cn(stl.wrapper, { [stl.active]: showContent })} onClick={() => activeTarget(index)}>
<div className={cn(stl.wrapper, 'rounded-xl', { [stl.active]: showContent })} onClick={() => activeTarget(index)}>
<div className={stl.top}>
{/* @ts-ignore */}
<Tooltip position="top" title="Rank of the most clicked element">
<div className={stl.index}>{index + 1}</div>
</Tooltip>
<div className="truncate">{target.selector}</div>
<div className="truncate font-mono">{target.selector}</div>
</div>
{showContent && (
<div className={stl.counts}>
<div>
{target.count} Clicks - {target.percent}%
{target.count} Click{target.count > 1 ? 's' : ''} - {target.percent}%
</div>
<div className="color-gray-medium">TOTAL CLICKS</div>
<div className="text-neutral-400">TOTAL CLICKS</div>
</div>
)}
</div>

View file

@ -17,12 +17,14 @@ import {
} from 'recharts';
import { durationFromMsFormatted } from 'App/date';
import { formatBytes } from 'App/utils';
import {Tooltip as TooltipANT} from 'antd';
import stl from './performance.module.css';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import { useStore } from 'App/mstore'
import { Segmented } from 'antd'
const CPU_VISUAL_OFFSET = 10;
@ -457,15 +459,33 @@ function Performance() {
return (
<BottomBlock>
<BottomBlock.Header>
<div className="flex items-center w-full">
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
<InfoLine>
<InfoLine.Point
label="Device Heap Size"
value={formatBytes(userDeviceHeapSize)}
display={true}
/>
</InfoLine>
<div className="flex items-center justify-between w-full">
<div className="flex gap-3 items-center">
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
<InfoLine>
<InfoLine.Point
label="Device Heap Size"
value={formatBytes(userDeviceHeapSize)}
display={true}
/>
</InfoLine>
</div>
<div className={'flex items-center gap-3'}>
<Segmented
options={[
{ label: (
<TooltipANT title="Performance overview isn't supported across tabs.">
<span>All Tabs</span>
</TooltipANT>
), value: 'all', disabled: true, },
{ label: 'Current Tab', value: 'current' },
]}
defaultValue="current"
size="small"
className="rounded-full font-medium"
/>
</div>
</div>
</BottomBlock.Header>
<BottomBlock.Content>

View file

@ -33,8 +33,8 @@ function AssistSessionsModal(props: ConnectProps) {
const sortOptions = metaList
.map((i: any) => ({
label: capitalize(i),
value: i
label: capitalize(i.key),
value: i.key
}));
React.useEffect(() => {

View file

@ -34,6 +34,7 @@ import { Icon } from 'UI';
import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton';
import ControlButton from './ControlButton';
import { WebEventsList } from "./EventsList";
import Timeline from './Timeline';
import PlayerControls from './components/PlayerControls';
import styles from './controls.module.css';

View file

@ -4,10 +4,12 @@ import { PlayerContext, MobilePlayerContext } from 'Components/Session/playerCon
import { observer } from 'mobx-react-lite';
import { getTimelinePosition } from './getTimelinePosition'
function EventsList({ scale }: { scale: number }) {
function EventsList() {
const { store } = useContext(PlayerContext);
const { tabStates, eventCount } = store.get();
const { eventCount, endTime } = store.get();
const tabStates = store.get().tabStates;
const scale = 100 / endTime;
const events = React.useMemo(() => {
return Object.values(tabStates)[0]?.eventList.filter(e => e.time) || [];
}, [eventCount]);
@ -34,11 +36,12 @@ function EventsList({ scale }: { scale: number }) {
);
}
function MobileEventsList({ scale }: { scale: number }) {
function MobileEventsList() {
const { store } = useContext(MobilePlayerContext);
const { eventList } = store.get();
const { eventList, endTime } = store.get();
const events = eventList.filter(e => e.type !== 'SWIPE')
const scale = 100/endTime;
return (
<>
{events.map((e) => (

View file

@ -13,11 +13,7 @@ import NotesList from './NotesList';
import SkipIntervalsList from './SkipIntervalsList';
import TimelineTracker from 'Components/Session_/Player/Controls/TimelineTracker';
interface IProps {
isMobile?: boolean;
}
function Timeline(props: IProps) {
function Timeline({ isMobile }: { isMobile: boolean }) {
const { player, store } = useContext(PlayerContext);
const [wasPlaying, setWasPlaying] = useState(false);
const [maxWidth, setMaxWidth] = useState(0);
@ -158,7 +154,7 @@ function Timeline(props: IProps) {
{devtoolsLoading || domLoading || !ready ? <div className={stl.stripes} /> : null}
</div>
{props.isMobile ? <MobEventsList scale={scale} /> : <WebEventsList scale={scale} />}
{isMobile ? <MobEventsList /> : <WebEventsList />}
<NotesList scale={scale} />
<SkipIntervalsList scale={scale} />

View file

@ -29,7 +29,7 @@ function TimelineZoomButton() {
}, [])
return (
<Tooltip title="Select a portion of the timeline to view the x-ray and activity for that specific selection." placement='top'>
<Button onClick={onClickHandler} size={'small'} className={'flex items-center font-semibold'}>
<Button onClick={onClickHandler} size={'small'} className={'flex items-center font-medium'}>
Focus Mode: {enabled ? 'On' : 'Off'}
</Button>
</Tooltip>

View file

@ -1,18 +1,24 @@
import React from 'react';
import { useStore } from 'App/mstore'
import { useStore } from 'App/mstore';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { JSONTree, NoContent, Tooltip } from 'UI';
import { formatMs } from 'App/date';
import diff from 'microdiff'
import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player';
import diff from 'microdiff';
import {
STORAGE_TYPES,
selectStorageList,
selectStorageListNow,
selectStorageType,
} from 'Player';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock/index';
import DiffRow from './DiffRow';
import cn from 'classnames';
import stl from './storage.module.css';
import logger from "App/logger";
import ReduxViewer from './ReduxViewer'
import logger from 'App/logger';
import ReduxViewer from './ReduxViewer';
import { Segmented } from 'antd'
function getActionsName(type: string) {
switch (type) {
@ -31,7 +37,7 @@ const storageDecodeKeys = {
[STORAGE_TYPES.ZUSTAND]: ['state', 'mutation'],
[STORAGE_TYPES.MOBX]: ['payload'],
[STORAGE_TYPES.NONE]: ['state, action', 'payload', 'mutation'],
}
};
function Storage() {
const { uiPlayerStore } = useStore();
@ -42,49 +48,48 @@ function Storage() {
const [stateObject, setState] = React.useState({});
const { player, store } = React.useContext(PlayerContext);
const { tabStates, currentTab } = store.get()
const state = tabStates[currentTab] || {}
const { tabStates, currentTab } = store.get();
const state = tabStates[currentTab] || {};
const listNow = selectStorageListNow(state) || [];
const list = selectStorageList(state) || [];
const type = selectStorageType(state) || STORAGE_TYPES.NONE
const type = selectStorageType(state) || STORAGE_TYPES.NONE;
React.useEffect(() => {
let currentState;
if (listNow.length === 0) {
currentState = decodeMessage(list[0])
currentState = decodeMessage(list[0]);
} else {
currentState = decodeMessage(listNow[listNow.length - 1])
currentState = decodeMessage(listNow[listNow.length - 1]);
}
const stateObj = currentState?.state || currentState?.payload?.state || {}
const stateObj = currentState?.state || currentState?.payload?.state || {};
const newState = Object.assign(stateObject, stateObj);
setState(newState);
}, [listNow.length]);
const decodeMessage = (msg: any) => {
const decoded = {};
const pureMSG = { ...msg }
const pureMSG = { ...msg };
const keys = storageDecodeKeys[type];
try {
keys.forEach(key => {
keys.forEach((key) => {
if (pureMSG[key]) {
// @ts-ignore TODO: types for decoder
decoded[key] = player.decodeMessage(pureMSG[key]);
}
});
} catch (e) {
logger.error("Error on message decoding: ", e, pureMSG);
logger.error('Error on message decoding: ', e, pureMSG);
return null;
}
return { ...pureMSG, ...decoded };
}
};
const decodedList = React.useMemo(() => {
return listNow.map(msg => {
return decodeMessage(msg)
})
}, [listNow.length])
return listNow.map((msg) => {
return decodeMessage(msg);
});
}, [listNow.length]);
const focusNextButton = () => {
if (lastBtnRef.current) {
@ -99,7 +104,10 @@ function Storage() {
focusNextButton();
}, [listNow]);
const renderDiff = (item: Record<string, any>, prevItem?: Record<string, any>) => {
const renderDiff = (
item: Record<string, any>,
prevItem?: Record<string, any>
) => {
if (!showDiffs) {
return;
}
@ -113,7 +121,10 @@ function Storage() {
if (!stateDiff) {
return (
<div style={{ flex: 3 }} className="flex flex-col p-2 pr-0 font-mono text-disabled-text">
<div
style={{ flex: 3 }}
className="flex flex-col p-2 pr-0 font-mono text-disabled-text"
>
No diff
</div>
);
@ -121,13 +132,15 @@ function Storage() {
return (
<div style={{ flex: 3 }} className="flex flex-col p-1 font-mono">
{stateDiff.map((d: Record<string, any>, i: number) => renderDiffs(d, i))}
{stateDiff.map((d: Record<string, any>, i: number) =>
renderDiffs(d, i)
)}
</div>
);
};
const renderDiffs = (diff: Record<string, any>, i: number) => {
const path = diff.path.join('.')
const path = diff.path.join('.');
return (
<React.Fragment key={i}>
<DiffRow path={path} diff={diff} />
@ -145,12 +158,16 @@ function Storage() {
player.jump(list[listNow.length].time);
};
const renderItem = (item: Record<string, any>, i: number, prevItem?: Record<string, any>) => {
const renderItem = (
item: Record<string, any>,
i: number,
prevItem?: Record<string, any>
) => {
let src;
let name;
const itemD = item
const prevItemD = prevItem ? prevItem : undefined
const itemD = item;
const prevItemD = prevItem ? prevItem : undefined;
switch (type) {
case STORAGE_TYPES.REDUX:
@ -177,7 +194,10 @@ function Storage() {
return (
<div
className={cn('flex justify-between items-start', src !== null ? 'border-b' : '')}
className={cn(
'flex justify-between items-start',
src !== null ? 'border-b' : ''
)}
key={`store-${i}`}
>
{src === null ? (
@ -187,7 +207,10 @@ function Storage() {
) : (
<>
{renderDiff(itemD, prevItemD)}
<div style={{ flex: 2 }} className={cn("flex pt-2", showDiffs && 'pl-10')}>
<div
style={{ flex: 2 }}
className={cn('flex pt-2', showDiffs && 'pl-10')}
>
<JSONTree
name={ensureString(name)}
src={src}
@ -202,11 +225,16 @@ function Storage() {
className="flex-1 flex gap-2 pt-2 items-center justify-end self-start"
>
{typeof item?.duration === 'number' && (
<div className="font-size-12 color-gray-medium">{formatMs(itemD.duration)}</div>
<div className="font-size-12 color-gray-medium">
{formatMs(itemD.duration)}
</div>
)}
<div className="w-12">
{i + 1 < listNow.length && (
<button className={stl.button} onClick={() => player.jump(item.time)}>
<button
className={stl.button}
onClick={() => player.jump(item.time)}
>
{'JUMP'}
</button>
)}
@ -222,31 +250,36 @@ function Storage() {
};
if (type === STORAGE_TYPES.REDUX) {
return <ReduxViewer />
return <ReduxViewer />;
}
return (
<BottomBlock>
{/*@ts-ignore*/}
<>
<BottomBlock.Header>
{list.length > 0 && (
<div className="flex w-full">
<h3 style={{ width: '25%', marginRight: 20 }} className="font-semibold">
{'STATE'}
</h3>
{showDiffs ? (
<h3 style={{ width: '39%' }} className="font-semibold">
DIFFS
</h3>
) : null}
<h3 style={{ width: '30%' }} className="font-semibold">
{getActionsName(type)}
</h3>
<h3 style={{ paddingRight: 30, marginLeft: 'auto' }} className="font-semibold">
<Tooltip title="Time to execute">TTE</Tooltip>
</h3>
<div className="flex w-full items-center">
<div
style={{ width: '25%', marginRight: 20 }}
className="font-semibold flex items-center gap-2"
>
<h3>{'STATE'}</h3>
</div>
)}
{showDiffs ? (
<h3 style={{ width: '39%' }} className="font-semibold">
DIFFS
</h3>
) : null}
<h3 style={{ width: '30%' }} className="font-semibold">
{getActionsName(type)}
</h3>
<h3
style={{ paddingRight: 30, marginLeft: 'auto' }}
className="font-semibold"
>
<Tooltip title="Time to execute">TTE</Tooltip>
</h3>
<Segmented options={[{ label: 'Current Tab', value: 'all' }]} />
</div>
</BottomBlock.Header>
<BottomBlock.Content className="flex">
<NoContent
@ -307,7 +340,10 @@ function Storage() {
.
<br />
<br />
<button className="color-teal" onClick={() => hideHint('storage')}>
<button
className="color-teal"
onClick={() => hideHint('storage')}
>
Got It!
</button>
</>
@ -322,8 +358,7 @@ function Storage() {
{'Empty state.'}
</div>
) : (
<JSONTree collapsed={2} src={stateObject}
/>
<JSONTree collapsed={2} src={stateObject} />
)}
</div>
<div className="flex" style={{ width: '75%' }}>
@ -342,7 +377,6 @@ function Storage() {
export default observer(Storage);
/**
* TODO: compute diff and only decode the required parts
* WIP example
@ -384,4 +418,4 @@ export default observer(Storage);
* }, [list.length])
* }
*
* */
* */

View file

@ -70,6 +70,7 @@ function SpotConsole({ onClose }: { onClose: () => void }) {
jump={jump}
iconProps={getIconProps(log.level)}
renderWithNL={renderWithNL}
showSingleTab
/>
))}
</VList>

View file

@ -143,7 +143,7 @@ function SpotPlayerHeader({
{browserVersion && (
<>
<div>·</div>
<div className="capitalize">Chrome v{browserVersion}</div>
<div>Chromium v{browserVersion}</div>
</>
)}
{resolution && (

View file

@ -3,15 +3,15 @@ import cn from 'classnames';
import cls from './infoLine.module.css';
const InfoLine = ({ children }) => (
<div className={ cls.info }>
<div className={ cn(cls.info, 'text-sm')}>
{ children }
</div>
)
const Point = ({ label = '', value = '', display=true, color, dotColor }) => display
? <div className={ cls.infoPoint } style={{ color }}>
? <div className={ cn(cls.infoPoint, 'text-sm') } style={{ color }}>
{ dotColor != null && <div className={ cn(cls.dot, `bg-${dotColor}`) } /> }
<span className={cls.label}>{ `${label}` }</span> { value }
<span className={cn(cls.label, 'text-sm')}>{ `${label}` }</span> { value }
</div>
: null;

View file

@ -1,7 +1,9 @@
import React, { useEffect, useRef, useState, useMemo } from 'react';
import { LogLevel, ILog } from 'Player';
import BottomBlock from '../BottomBlock';
import { Tabs, Input, Icon, NoContent } from 'UI';
import { Tabs, Icon, NoContent } from 'UI';
import {Input} from 'antd';
import {SearchOutlined, InfoCircleOutlined} from '@ant-design/icons';
import cn from 'classnames';
import ConsoleRow from '../ConsoleRow';
import { PlayerContext } from 'App/components/Session/playerContext';
@ -9,6 +11,7 @@ import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
import { useModal } from 'App/components/Modal';
import TabSelector from "../TabSelector";
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import { VList, VListHandle } from "virtua";
@ -93,6 +96,7 @@ function ConsolePanel({
sessionStore: { devTools },
uiPlayerStore,
} = useStore();
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
@ -109,29 +113,34 @@ function ConsolePanel({
const jump = (t: number) => player.jump(t);
const { currentTab, tabStates } = store.get();
const {
logList = [],
exceptionsList = [],
logListNow = [],
exceptionsListNow = [],
} = tabStates[currentTab] ?? {};
const tabsArr = Object.keys(tabStates);
const tabValues = Object.values(tabStates);
const dataSource = uiPlayerStore.dataSource;
const showSingleTab = dataSource === 'current';
const { logList = [], exceptionsList = [], logListNow = [], exceptionsListNow = [] } = React.useMemo(() => {
if (showSingleTab) {
return tabStates[currentTab] ?? {};
} else {
const logList = tabValues.flatMap(tab => tab.logList);
const exceptionsList = tabValues.flatMap(tab => tab.exceptionsList);
const logListNow = isLive ? tabValues.flatMap(tab => tab.logListNow) : [];
const exceptionsListNow = isLive ? tabValues.flatMap(tab => tab.exceptionsListNow) : [];
return { logList, exceptionsList, logListNow, exceptionsListNow }
}
}, [currentTab, tabStates, dataSource, tabValues, isLive])
const getTabNum = (tab: string) => (tabsArr.findIndex((t) => t === tab) + 1);
const list = isLive
? (useMemo(
() => logListNow.concat(exceptionsListNow).sort((a, b) => a.time - b.time),
[logListNow.length, exceptionsListNow.length]
) as ILog[])
: (useMemo(
() => logList.concat(exceptionsList).sort((a, b) => a.time - b.time),
[logList.length, exceptionsList.length]
).filter((l) =>
zoomEnabled ? l.time >= zoomStartTs && l.time <= zoomEndTs : true
) as ILog[]);
const list = useMemo(() => {
if (isLive) {
return logListNow.concat(exceptionsListNow).sort((a, b) => a.time - b.time)
} else {
const logs = logList.concat(exceptionsList).sort((a, b) => a.time - b.time)
return zoomEnabled ? logs.filter(l => l.time >= zoomStartTs && l.time <= zoomEndTs) : logs
}
}, [isLive, logList.length, exceptionsList.length, logListNow.length, exceptionsListNow.length, zoomEnabled, zoomStartTs, zoomEndTs])
let filteredList = useRegExListFilterMemo(list, (l) => l.value, filter);
filteredList = useTabListFilterMemo(filteredList, (l) => LEVEL_TAB[l.level], ALL, activeTab);
React.useEffect(() => {
}, [activeTab, filter]);
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: any) =>
devTools.update(INDEX_KEY, { filter: value });
@ -180,23 +189,26 @@ function ConsolePanel({
<span className="font-semibold color-gray-medium mr-4">Console</span>
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={false} />
</div>
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
/>
<div className={'flex items-center gap-2'}>
<TabSelector />
<Input
className="rounded-lg"
placeholder="Filter by keyword"
name="filter"
onChange={onFilterChange}
value={filter}
size='small'
prefix={<SearchOutlined className='text-neutral-400' />}
/>
</div>
{/* @ts-ignore */}
</BottomBlock.Header>
{/* @ts-ignore */}
<BottomBlock.Content className="overflow-y-auto">
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
<div className="capitalize flex items-center mt-16 gap-2">
<InfoCircleOutlined size={18} />
No Data
</div>
}
@ -211,6 +223,8 @@ function ConsolePanel({
iconProps={getIconProps(log.level)}
renderWithNL={renderWithNL}
onClick={() => showDetails(log)}
showSingleTab={showSingleTab}
getTabNum={getTabNum}
/>
))}
</VList>

View file

@ -1,10 +1,13 @@
import React, { useEffect, useRef, useState } from 'react';
import { LogLevel, ILog } from 'Player';
import BottomBlock from '../BottomBlock';
import { Tabs, Input, Icon, NoContent } from 'UI';
import { Tabs, Input, NoContent } from 'UI';
import cn from 'classnames';
import ConsoleRow from '../ConsoleRow';
import { IOSPlayerContext, MobilePlayerContext } from 'App/components/Session/playerContext';
import {
IOSPlayerContext,
MobilePlayerContext,
} from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { VList, VListHandle } from 'virtua';
import { useStore } from 'App/mstore';
@ -12,6 +15,7 @@ import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorD
import { useModal } from 'App/components/Modal';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import { InfoCircleOutlined, SearchOutlined } from '@ant-design/icons';
const ALL = 'ALL';
const INFO = 'INFO';
@ -26,7 +30,10 @@ const LEVEL_TAB = {
[LogLevel.EXCEPTION]: ERRORS,
} as const;
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab }));
const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({
text: tab,
key: tab,
}));
function renderWithNL(s: string | null = '') {
if (typeof s !== 'string') return '';
@ -73,20 +80,23 @@ function MobileConsolePanel() {
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
const { showModal } = useModal();
const { player, store } = React.useContext<IOSPlayerContext>(MobilePlayerContext);
const { player, store } =
React.useContext<IOSPlayerContext>(MobilePlayerContext);
const jump = (t: number) => player.jump(t);
const {
logList,
logListNow,
exceptionsListNow,
} = store.get();
const { logList, logListNow, exceptionsListNow } = store.get();
const list = logList as ILog[];
let filteredList = useRegExListFilterMemo(list, (l) => l.value, filter);
filteredList = useTabListFilterMemo(filteredList, (l) => LEVEL_TAB[l.level], ALL, activeTab);
filteredList = useTabListFilterMemo(
filteredList,
(l) => LEVEL_TAB[l.level],
ALL,
activeTab
);
const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab });
const onTabClick = (activeTab: any) =>
devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: any) =>
devTools.update(INDEX_KEY, { filter: value });
@ -136,34 +146,35 @@ function MobileConsolePanel() {
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Console</span>
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={false} />
<Tabs
tabs={TABS}
active={activeTab}
onClick={onTabClick}
border={false}
/>
</div>
<Input
className="input-small h-8"
className="rounded-lg"
placeholder="Filter by keyword"
icon="search"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
size="small"
prefix={<SearchOutlined className="text-neutral-400" />}
/>
</BottomBlock.Header>
<BottomBlock.Content className="overflow-y-auto">
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
<div className="capitalize flex items-center mt-16 gap-2">
<InfoCircleOutlined size={18} />
No Data
</div>
}
size="small"
show={filteredList.length === 0}
>
<VList
ref={_list}
itemSize={25}
count={filteredList.length || 1}
>
<VList ref={_list} itemSize={25} count={filteredList.length || 1}>
{filteredList.map((log, index) => (
<ConsoleRow
key={log.time + index}
@ -172,6 +183,7 @@ function MobileConsolePanel() {
iconProps={getIconProps(log.level)}
renderWithNL={renderWithNL}
onClick={() => showDetails(log)}
showSingleTab
/>
))}
</VList>

View file

@ -2,6 +2,8 @@ import React, { useState } from 'react';
import cn from 'classnames';
import { Icon } from 'UI';
import JumpButton from 'Shared/DevTools/JumpButton';
import { Tag } from 'antd';
import TabTag from "../TabTag";
interface Props {
log: any;
@ -10,6 +12,8 @@ interface Props {
renderWithNL?: any;
style?: any;
onClick?: () => void;
getTabNum?: (tab: string) => number;
showSingleTab: boolean;
}
function ConsoleRow(props: Props) {
const { log, iconProps, jump, renderWithNL, style } = props;
@ -41,11 +45,13 @@ function ConsoleRow(props: Props) {
const titleLine = lines[0];
const restLines = lines.slice(1);
const logSource = props.showSingleTab ? -1 : props.getTabNum?.(log.tabId);
const logTabId = log.tabId
return (
<div
style={style}
className={cn(
'border-b flex items-start py-1 px-4 pe-8 overflow-hidden group relative',
'border-b border-neutral-950/5 flex items-start gap-2 py-1 px-4 pe-8 overflow-hidden group relative',
{
info: !log.isYellow && !log.isRed,
warn: log.isYellow,
@ -55,11 +61,10 @@ function ConsoleRow(props: Props) {
)}
onClick={clickable ? () => (!!log.errorId ? props.onClick?.() : toggleExpand()) : undefined}
>
<div className="mr-2">
<Icon size="14" {...iconProps} />
</div>
{logSource !== -1 && <TabTag logSource={logSource} logTabId={logTabId} />}
<Icon size="14" {...iconProps} className='mt-0.5' />
<div key={log.key} data-scroll-item={log.isRed}>
<div className="flex items-start text-sm ">
<div className="flex items-start text-sm">
<div className={cn('flex items-start', { 'cursor-pointer underline decoration-dotted decoration-gray-400': !!log.errorId })}>
{canExpand && (
<Icon name={expanded ? 'caret-down-fill' : 'caret-right-fill'} className="mr-2" />

View file

@ -1,6 +1,8 @@
import React from 'react';
import { Icon, Tooltip } from 'UI';
import { shortDurationFromMs } from "App/date";
import { Tooltip } from 'UI';
import { CaretRightOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { shortDurationFromMs } from 'App/date';
interface Props {
onClick: any;
@ -12,19 +14,24 @@ function JumpButton(props: Props) {
return (
<div className="absolute right-2 top-0 bottom-0 my-auto flex items-center">
<Tooltip title={tooltip} disabled={!tooltip}>
<div
className="border cursor-pointer hidden group-hover:flex rounded bg-white text-xs items-center px-2 py-1 color-teal hover:shadow h-6"
<Button
type="default"
size="small"
className="hidden group-hover:flex rounded-lg text-xs p-1 py-0 gap-0 h-6"
iconPosition="end"
onClick={(e: any) => {
e.stopPropagation();
props.onClick();
}}
icon={<CaretRightOutlined />}
>
<Icon name="caret-right-fill" size="12" color="teal" />
<span>JUMP</span>
</div>
{props.time ? <div className={'block group-hover:hidden mr-2'}>
{shortDurationFromMs(props.time)}
</div> : null}
JUMP
</Button>
{props.time ? (
<div className={'block group-hover:hidden mr-2 text-sm'}>
{shortDurationFromMs(props.time)}
</div>
) : null}
</Tooltip>
</div>
);

View file

@ -1,7 +1,6 @@
import { ResourceType, Timed } from 'Player';
import MobilePlayer from 'Player/mobile/IOSPlayer';
import WebPlayer from 'Player/web/WebPlayer';
import { Duration } from 'luxon';
import { observer } from 'mobx-react-lite';
import React, { useMemo, useState } from 'react';
@ -13,17 +12,19 @@ import {
import { formatMs } from 'App/date';
import { useStore } from 'App/mstore';
import { formatBytes } from 'App/utils';
import { Icon, Input, NoContent, Tabs, Toggler, Tooltip } from 'UI';
import { Icon, NoContent, Tabs } from 'UI';
import { Tooltip, Input, Switch, Form } from 'antd';
import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
import { WsChannel } from "App/player/web/messages";
import { WsChannel } from 'App/player/web/messages';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import TabSelector from '../TabSelector';
import TimeTable from '../TimeTable';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import WSModal from './WSModal';
import WSPanel from './WSPanel';
const INDEX_KEY = 'network';
@ -57,12 +58,6 @@ export const NETWORK_TABS = TAP_KEYS.map((tab) => ({
const DOM_LOADED_TIME_COLOR = 'teal';
const LOAD_TIME_COLOR = 'red';
function compare(a: any, b: any, key: string) {
if (a[key] > b[key]) return 1;
if (a[key] < b[key]) return -1;
return 0;
}
export function renderType(r: any) {
return (
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
@ -79,14 +74,6 @@ export function renderName(r: any) {
);
}
export function renderStart(r: any) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
</div>
);
}
function renderSize(r: any) {
if (r.responseBodySize) return formatBytes(r.responseBodySize);
let triggerText;
@ -125,13 +112,10 @@ export function renderDuration(r: any) {
if (!r.isRed && !r.isYellow) return text;
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
return (
@ -151,7 +135,7 @@ function renderStatus({
error?: string;
}) {
const displayedStatus = error ? (
<Tooltip delay={0} title={error}>
<Tooltip title={error}>
<div
style={{ width: 90 }}
className={'overflow-hidden overflow-ellipsis'}
@ -165,7 +149,7 @@ function renderStatus({
return (
<>
{cached ? (
<Tooltip title={'Served from cache'}>
<Tooltip title={'Served from cache'} placement="top">
<div className="flex items-center">
<span className="mr-1">{displayedStatus}</span>
<Icon name="wifi" size={16} />
@ -178,13 +162,10 @@ function renderStatus({
);
}
function NetworkPanelCont({
panelHeight,
}: {
panelHeight: number;
}) {
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
const { player, store } = React.useContext(PlayerContext);
const { sessionStore } = useStore();
const { sessionStore, uiPlayerStore } = useStore();
const startedAt = sessionStore.current.startedAt;
const {
domContentLoadedTime,
@ -192,7 +173,12 @@ function NetworkPanelCont({
domBuildingTime,
tabStates,
currentTab,
tabNames,
} = store.get();
const tabsArr = Object.keys(tabStates);
const tabValues = Object.values(tabStates);
const dataSource = uiPlayerStore.dataSource;
const showSingleTab = dataSource === 'current';
const {
fetchList = [],
resourceList = [],
@ -200,8 +186,34 @@ function NetworkPanelCont({
resourceListNow = [],
websocketList = [],
websocketListNow = [],
} = tabStates[currentTab];
} = React.useMemo(() => {
if (showSingleTab) {
return tabStates[currentTab] ?? {};
} else {
const fetchList = tabValues.flatMap((tab) => tab.fetchList);
const resourceList = tabValues.flatMap((tab) => tab.resourceList);
const fetchListNow = tabValues
.flatMap((tab) => tab.fetchListNow)
.filter(Boolean);
const resourceListNow = tabValues
.flatMap((tab) => tab.resourceListNow)
.filter(Boolean);
const websocketList = tabValues.flatMap((tab) => tab.websocketList);
const websocketListNow = tabValues
.flatMap((tab) => tab.websocketListNow)
.filter(Boolean);
return {
fetchList,
resourceList,
fetchListNow,
resourceListNow,
websocketList,
websocketListNow,
};
}
}, [currentTab, tabStates, dataSource, tabValues]);
const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1;
const getTabName = (tabId: string) => tabNames[tabId]
return (
<NetworkPanelComp
loadTime={loadTime}
@ -216,15 +228,14 @@ function NetworkPanelCont({
startedAt={startedAt}
websocketList={websocketList as WSMessage[]}
websocketListNow={websocketListNow as WSMessage[]}
getTabNum={getTabNum}
getTabName={getTabName}
showSingleTab={showSingleTab}
/>
);
}
function MobileNetworkPanelCont({
panelHeight,
}: {
panelHeight: number;
}) {
function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
const { player, store } = React.useContext(MobilePlayerContext);
const { uiPlayerStore, sessionStore } = useStore();
const startedAt = sessionStore.current.startedAt;
@ -301,6 +312,9 @@ interface Props {
onClose?: () => void;
activeOutsideIndex?: number;
isSpot?: boolean;
getTabNum?: (tab: string) => number;
getTabName?: (tabId: string) => string;
showSingleTab?: boolean;
}
export const NetworkPanelComp = observer(
@ -323,8 +337,13 @@ export const NetworkPanelComp = observer(
onClose,
activeOutsideIndex,
isSpot,
getTabNum,
showSingleTab,
getTabName,
}: Props) => {
const [selectedWsChannel, setSelectedWsChannel] = React.useState<WsChannel[] | null>(null)
const [selectedWsChannel, setSelectedWsChannel] = React.useState<
WsChannel[] | null
>(null);
const { showModal } = useModal();
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
@ -480,10 +499,10 @@ export const NetworkPanelComp = observer(
const showDetailsModal = (item: any) => {
if (item.type === 'websocket') {
const socketMsgList = websocketList.filter(
(ws) => ws.channelName === item.channelName
);
(ws) => ws.channelName === item.channelName
);
return setSelectedWsChannel(socketMsgList)
return setSelectedWsChannel(socketMsgList);
}
setIsDetailsModalActive(true);
showModal(
@ -507,6 +526,61 @@ export const NetworkPanelComp = observer(
stopAutoscroll();
};
const tableCols = React.useMemo(() => {
const cols: any[] = [
{
label: 'Status',
dataKey: 'status',
width: 90,
render: renderStatus,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
},
{
label: 'Method',
width: 80,
dataKey: 'method',
},
{
label: 'Name',
width: 240,
dataKey: 'name',
render: renderName,
},
{
label: 'Size',
width: 80,
dataKey: 'decodedBodySize',
render: renderSize,
hidden: activeTab === XHR,
},
{
label: 'Duration',
width: 80,
dataKey: 'duration',
render: renderDuration,
},
];
if (!showSingleTab && !isSpot) {
cols.unshift({
label: 'Source',
width: 64,
render: (r: Record<string, any>) => (
<Tooltip title={`${getTabName?.(r.tabId) ?? `Tab ${getTabNum?.(r.tabId) ?? 0}`}`} placement="left">
<div className="bg-gray-light rounded-full min-w-5 min-h-5 w-5 h-5 flex items-center justify-center text-xs cursor-default">
{getTabNum?.(r.tabId) ?? 0}
</div>
</Tooltip>
),
});
}
return cols;
}, [showSingleTab]);
return (
<BottomBlock
style={{ height: '100%' }}
@ -529,26 +603,39 @@ export const NetworkPanelComp = observer(
/>
)}
</div>
<Input
className="input-small"
placeholder="Filter by name, type, method or value"
icon="search"
name="filter"
onChange={onFilterChange}
height={28}
width={280}
value={filter}
/>
<div className={'flex items-center gap-2'}>
{!isMobile && !isSpot ? <TabSelector /> : null}
<Input
className="rounded-lg"
placeholder="Filter by name, type, method or value"
name="filter"
onChange={onFilterChange}
width={280}
value={filter}
size="small"
prefix={<SearchOutlined className="text-neutral-400" />}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
<div>
<Toggler
checked={showOnlyErrors}
name="show-errors-only"
onChange={() => setShowOnlyErrors(!showOnlyErrors)}
label="4xx-5xx Only"
/>
<Form.Item name="show-errors-only" className="mb-0">
<label
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}}
>
<Switch
checked={showOnlyErrors}
onChange={() => setShowOnlyErrors(!showOnlyErrors)}
size="small"
/>
<span className="text-sm ms-2">4xx-5xx Only</span>
</label>
</Form.Item>
</div>
<InfoLine>
<InfoLine.Point
@ -588,8 +675,8 @@ export const NetworkPanelComp = observer(
</div>
<NoContent
title={
<div className="capitalize flex items-center">
<Icon name="info-circle" className="mr-2" size="18" />
<div className="capitalize flex items-center gap-2">
<InfoCircleOutlined size={18} />
No Data
</div>
}
@ -613,52 +700,13 @@ export const NetworkPanelComp = observer(
}}
activeIndex={activeIndex}
>
{[
// {
// label: 'Start',
// width: 120,
// render: renderStart,
// },
{
label: 'Status',
dataKey: 'status',
width: 90,
render: renderStatus,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
},
{
label: 'Method',
width: 80,
dataKey: 'method',
},
{
label: 'Name',
width: 240,
dataKey: 'name',
render: renderName,
},
{
label: 'Size',
width: 80,
dataKey: 'decodedBodySize',
render: renderSize,
hidden: activeTab === XHR,
},
{
label: 'Duration',
width: 80,
dataKey: 'duration',
render: renderDuration,
},
]}
{tableCols}
</TimeTable>
{selectedWsChannel ? (
<WSPanel socketMsgList={selectedWsChannel} onClose={() => setSelectedWsChannel(null)} />
<WSPanel
socketMsgList={selectedWsChannel}
onClose={() => setSelectedWsChannel(null)}
/>
) : null}
</NoContent>
</BottomBlock.Content>

View file

@ -23,9 +23,9 @@ const lineLength = 40;
function WSPanel({ socketMsgList, onClose }: Props) {
const [query, setQuery] = React.useState('');
const [list, setList] = React.useState(socketMsgList);
const [selectedRow, setSelectedRow] = React.useState<SocketMsg | null>(null);
const [selectedRow, setSelectedRow] = React.useState<{ msg: SocketMsg, id: number } | null>(null);
const onQueryChange = (e) => {
const onQueryChange = (e: any) => {
setQuery(e.target.value);
const newList = filterList(socketMsgList, e.target.value, [
'data',
@ -69,15 +69,16 @@ function WSPanel({ socketMsgList, onClose }: Props) {
position: 'relative',
}}
>
{list.map((msg) => (
{list.map((msg, i) => (
<Row
msg={msg}
key={msg.timestamp}
onSelect={() => setSelectedRow(msg)}
onSelect={() => setSelectedRow({ msg, id: i })}
isSelected={selectedRow ? selectedRow.id === i : false}
/>
))}
{selectedRow ? (
<SelectedRow msg={selectedRow} onClose={() => setSelectedRow(null)} />
<SelectedRow msg={selectedRow.msg} onClose={() => setSelectedRow(null)} />
) : null}
</div>
</div>
@ -127,7 +128,7 @@ function MsgDirection({ dir }: { dir: 'up' | 'down' }) {
);
}
function Row({ msg, onSelect }: { msg: SocketMsg; onSelect: () => void }) {
function Row({ msg, onSelect, isSelected }: { msg: SocketMsg; isSelected: boolean; onSelect: () => void }) {
return (
<>
<div
@ -149,7 +150,7 @@ function Row({ msg, onSelect }: { msg: SocketMsg; onSelect: () => void }) {
'rounded-full font-bold text-xl p-2 bg-white w-6 h-6 flex items-center justify-center'
}
>
<span>{isOpen ? '-' : '+'}</span>
<span>{isSelected ? '-' : '+'}</span>
</div>
) : null}
</div>

View file

@ -1,8 +1,13 @@
import { Timed } from 'Player';
import React, { useEffect, useMemo, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { Tabs, Input, NoContent, Icon } from 'UI';
import { PlayerContext, MobilePlayerContext } from 'App/components/Session/playerContext';
import { Tabs, NoContent, Icon } from 'UI';
import { Input } from 'antd';
import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons';
import {
PlayerContext,
MobilePlayerContext,
} from 'App/components/Session/playerContext';
import BottomBlock from '../BottomBlock';
import { useModal } from 'App/components/Modal';
import { useStore } from 'App/mstore';
@ -10,6 +15,7 @@ import { typeList } from 'Types/session/stackEvent';
import StackEventRow from 'Shared/DevTools/StackEventRow';
import StackEventModal from '../StackEventModal';
import { Segmented, Tooltip } from 'antd';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import { VList, VListHandle } from 'virtua';
@ -24,198 +30,247 @@ const ALL = 'ALL';
const TAB_KEYS = [ALL, ...typeList] as const;
const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab }));
type EventsList = Array<Timed & { name: string; source: string; key: string; payload?: string[] }>;
type EventsList = Array<
Timed & { name: string; source: string; key: string; payload?: string[] }
>;
const WebStackEventPanelComp = observer(
() => {
const { uiPlayerStore } = useStore();
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
const { player, store } = React.useContext(PlayerContext);
const jump = (t: number) => player.jump(t);
const { currentTab, tabStates } = store.get();
const WebStackEventPanelComp = observer(() => {
const { uiPlayerStore } = useStore();
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
const { player, store } = React.useContext(PlayerContext);
const jump = (t: number) => player.jump(t);
const { currentTab, tabStates } = store.get();
const { stackList: list = [], stackListNow: listNow = [] } = tabStates[currentTab];
const { stackList: list = [], stackListNow: listNow = [] } =
tabStates[currentTab];
return (
<EventsPanel
list={list as EventsList}
listNow={listNow as EventsList}
jump={jump}
zoomEnabled={zoomEnabled}
zoomStartTs={zoomStartTs}
zoomEndTs={zoomEndTs}
/>
);
}
);
return (
<EventsPanel
list={list as EventsList}
listNow={listNow as EventsList}
jump={jump}
zoomEnabled={zoomEnabled}
zoomStartTs={zoomStartTs}
zoomEndTs={zoomEndTs}
/>
);
});
export const WebStackEventPanel = WebStackEventPanelComp;
const MobileStackEventPanelComp = observer(
() => {
const { uiPlayerStore } = useStore();
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
const { player, store } = React.useContext(MobilePlayerContext);
const jump = (t: number) => player.jump(t);
const { eventList: list = [], eventListNow: listNow = [] } = store.get();
const MobileStackEventPanelComp = observer(() => {
const { uiPlayerStore } = useStore();
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
const { player, store } = React.useContext(MobilePlayerContext);
const jump = (t: number) => player.jump(t);
const { eventList: list = [], eventListNow: listNow = [] } = store.get();
return (
<EventsPanel
list={list as EventsList}
listNow={listNow as EventsList}
jump={jump}
zoomEnabled={zoomEnabled}
zoomStartTs={zoomStartTs}
zoomEndTs={zoomEndTs}
/>
);
}
);
return (
<EventsPanel
list={list as EventsList}
listNow={listNow as EventsList}
jump={jump}
isMobile
zoomEnabled={zoomEnabled}
zoomStartTs={zoomStartTs}
zoomEndTs={zoomEndTs}
/>
);
});
export const MobileStackEventPanel = MobileStackEventPanelComp;
const EventsPanel = observer(({
list,
listNow,
jump,
zoomEnabled,
zoomStartTs,
zoomEndTs,
}: {
list: EventsList;
listNow: EventsList;
jump: (t: number) => void;
zoomEnabled: boolean;
zoomStartTs: number;
zoomEndTs: number;
}) => {
const {
sessionStore: { devTools },
} = useStore();
const { showModal } = useModal();
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); // TODO:embed that into useModal
const filter = devTools[INDEX_KEY].filter;
const activeTab = devTools[INDEX_KEY].activeTab;
const activeIndex = devTools[INDEX_KEY].index;
const EventsPanel = observer(
({
list,
listNow,
jump,
zoomEnabled,
zoomStartTs,
zoomEndTs,
isMobile,
}: {
list: EventsList;
listNow: EventsList;
jump: (t: number) => void;
zoomEnabled: boolean;
zoomStartTs: number;
zoomEndTs: number;
isMobile?: boolean;
}) => {
const {
sessionStore: { devTools },
} = useStore();
const { showModal } = useModal();
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); // TODO:embed that into useModal
const filter = devTools[INDEX_KEY].filter;
const activeTab = devTools[INDEX_KEY].activeTab;
const activeIndex = devTools[INDEX_KEY].index;
const inZoomRangeList = list.filter(({ time }) =>
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
);
const inZoomRangeListNow = listNow.filter(({ time }) =>
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
);
const inZoomRangeList = list.filter(({ time }) =>
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
);
const inZoomRangeListNow = listNow.filter(({ time }) =>
zoomEnabled ? zoomStartTs <= time && time <= zoomEndTs : true
);
let filteredList = useRegExListFilterMemo(inZoomRangeList, (it) => {
const searchBy = [it.name]
if (it.payload) {
const payload = Array.isArray(it.payload) ? it.payload.join(',') : JSON.stringify(it.payload);
searchBy.push(payload);
}
return searchBy
}, filter);
filteredList = useTabListFilterMemo(filteredList, (it) => it.source, ALL, activeTab);
const onTabClick = (activeTab: (typeof TAB_KEYS)[number]) =>
devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => devTools.update(INDEX_KEY, { filter: value });
const tabs = useMemo(
() => TABS.filter(({ key }) => key === ALL || inZoomRangeList.some(({ source }) => key === source)),
[inZoomRangeList.length]
);
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
filteredList,
getLastItemTime(inZoomRangeListNow),
activeIndex,
(index) => devTools.update(INDEX_KEY, { index })
);
const onMouseEnter = stopAutoscroll;
const onMouseLeave = () => {
if (isDetailsModalActive) {
return;
}
timeoutStartAutoscroll();
};
const showDetails = (item: any) => {
setIsDetailsModalActive(true);
showModal(<StackEventModal event={item} />, {
right: true,
width: 500,
onClose: () => {
setIsDetailsModalActive(false);
timeoutStartAutoscroll();
let filteredList = useRegExListFilterMemo(
inZoomRangeList,
(it) => {
const searchBy = [it.name];
if (it.payload) {
const payload = Array.isArray(it.payload)
? it.payload.join(',')
: JSON.stringify(it.payload);
searchBy.push(payload);
}
return searchBy;
},
});
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
stopAutoscroll();
};
filter
);
filteredList = useTabListFilterMemo(
filteredList,
(it) => it.source,
ALL,
activeTab
);
const _list = React.useRef<VListHandle>(null);
useEffect(() => {
if (_list.current) {
_list.current.scrollToIndex(activeIndex);
}
}, [activeIndex]);
const onTabClick = (activeTab: (typeof TAB_KEYS)[number]) =>
devTools.update(INDEX_KEY, { activeTab });
const onFilterChange = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) =>
devTools.update(INDEX_KEY, { filter: value });
const tabs = useMemo(
() =>
TABS.filter(
({ key }) =>
key === ALL || inZoomRangeList.some(({ source }) => key === source)
),
[inZoomRangeList.length]
);
return (
<BottomBlock style={{ height: '100%' }} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Stack Events</span>
<Tabs
renameTab={mapNames}
tabs={tabs}
active={activeTab}
onClick={onTabClick}
border={false}
/>
</div>
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
/>
</BottomBlock.Header>
<BottomBlock.Content className="overflow-y-auto">
<NoContent
title={
<div className="capitalize flex items-center mt-16">
<Icon name="info-circle" className="mr-2" size="18" />
No Data
</div>
}
size="small"
show={filteredList.length === 0}
>
<VList
ref={_list}
count={filteredList.length || 1}
>
{filteredList.map((item, index) => (
<StackEventRow
isActive={activeIndex === index}
key={item.key}
event={item}
onJump={() => {
stopAutoscroll();
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
jump(item.time);
}}
onClick={() => showDetails(item)}
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
filteredList,
getLastItemTime(inZoomRangeListNow),
activeIndex,
(index) => devTools.update(INDEX_KEY, { index })
);
const onMouseEnter = stopAutoscroll;
const onMouseLeave = () => {
if (isDetailsModalActive) {
return;
}
timeoutStartAutoscroll();
};
const showDetails = (item: any) => {
setIsDetailsModalActive(true);
showModal(<StackEventModal event={item} />, {
right: true,
width: 500,
onClose: () => {
setIsDetailsModalActive(false);
timeoutStartAutoscroll();
},
});
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
stopAutoscroll();
};
const _list = React.useRef<VListHandle>(null);
useEffect(() => {
if (_list.current) {
_list.current.scrollToIndex(activeIndex);
}
}, [activeIndex]);
return (
<BottomBlock
style={{ height: '100%' }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<BottomBlock.Header>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">
Stack Events
</span>
<Tabs
renameTab={mapNames}
tabs={tabs}
active={activeTab}
onClick={onTabClick}
border={false}
/>
</div>
<div className={'flex items-center gap-2'}>
{isMobile ? null : (
<Segmented
options={[
{ label: 'All Tabs', value: 'all' },
{
label: (
<Tooltip title="Stack Events overview is available only for all tabs combined.">
<span>Current Tab</span>
</Tooltip>
),
value: 'current',
disabled: true,
},
]}
defaultValue="all"
size="small"
className="rounded-full font-medium"
/>
))}
</VList>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
);
});
)}
<Input
className="rounded-lg"
placeholder="Filter by keyword"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
size="small"
prefix={<SearchOutlined className="text-neutral-400" />}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content className="overflow-y-auto">
<NoContent
title={
<div className="capitalize flex items-center mt-16 gap-2">
<InfoCircleOutlined size={18} />
No Data
</div>
}
size="small"
show={filteredList.length === 0}
>
<VList ref={_list} count={filteredList.length || 1}>
{filteredList.map((item, index) => (
<StackEventRow
isActive={activeIndex === index}
key={item.key}
event={item}
onJump={() => {
stopAutoscroll();
devTools.update(INDEX_KEY, {
index: filteredList.indexOf(item),
});
jump(item.time);
}}
onClick={() => showDetails(item)}
/>
))}
</VList>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
);
}
);

View file

@ -0,0 +1,22 @@
import React from 'react'
import { Segmented } from 'antd'
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
function TabSelector() {
const { uiPlayerStore } = useStore();
const currentValue = uiPlayerStore.dataSource;
const options = [
{ label: 'All Tabs', value: 'all' },
{ label: 'Current Tab', value: 'current' }
]
const onChange = (value: 'all' | 'current') => {
uiPlayerStore.changeDataSource(value)
}
return (
<Segmented options={options} value={currentValue} onChange={onChange} className='font-medium rounded-lg' size='small' />
)
}
export default observer(TabSelector)

View file

@ -0,0 +1,23 @@
import React from 'react';
import { Tooltip } from 'antd';
import { observer } from 'mobx-react-lite';
import { PlayerContext } from 'Components/Session/playerContext';
function TabTag({ logSource, logTabId }: { logSource: number; logTabId: string }) {
const { store } = React.useContext(PlayerContext);
const { tabNames } = store.get();
return (
<Tooltip title={`${tabNames[logTabId] ?? `Tab ${logSource}`}`} placement="left">
<div
className={
'bg-gray-light rounded-full min-w-5 min-h-5 w-5 h-5 flex items-center justify-center text-xs cursor-default'
}
>
{logSource}
</div>
</Tooltip>
);
}
export default observer(TabTag);

View file

@ -199,7 +199,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
return (
<div
className={cn(
'dev-row border-b border-color-gray-light-shade group items-center',
'dev-row border-b border-neutral-950/5 group items-center text-sm',
stl.row,
{
[stl.hoverable]: hoverable,
@ -215,7 +215,7 @@ export default class TimeTable extends React.PureComponent<Props, State> {
{columns
.filter((i: any) => !i.hidden)
.map(({ dataKey, render, width, label }) => (
<div key={parseInt(label.replace(' ', '')+dataKey, 36)} className={cn(stl.cell, 'overflow-ellipsis overflow-hidden')} style={{ width: `${width}px` }}>
<div key={parseInt(label.replace(' ', '')+dataKey, 36)} className={cn(stl.cell, 'overflow-ellipsis overflow-hidden !py-0.5')} style={{ width: `${width}px` }}>
{render
? render(row)
: row[dataKey || ''] || <i className="color-gray-light">{'empty'}</i>}

View file

@ -8,7 +8,7 @@ import Select from 'react-select';
import cn from 'classnames';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { searchService} from 'App/services';
import { searchService } from 'App/services';
const dropdownStyles = {
option: (provided: any, state: any) => ({
@ -31,22 +31,18 @@ const dropdownStyles = {
backgroundColor: colors['active-blue']
}
}),
control: (provided: any) => {
const obj = {
...provided,
border: 'solid thin transparent !important',
backgroundColor: 'transparent',
cursor: 'pointer',
height: '26px',
minHeight: '26px',
borderRadius: '.5rem',
boxShadow: 'none !important'
};
return obj;
},
control: (provided: any) => ({
...provided,
border: 'solid thin transparent !important',
backgroundColor: 'transparent',
cursor: 'pointer',
height: '26px',
minHeight: '26px',
borderRadius: '.5rem',
boxShadow: 'none !important'
}),
valueContainer: (provided: any) => ({
...provided,
// paddingRight: '0px',
width: 'fit-content',
alignItems: 'center',
height: '26px',
@ -77,7 +73,6 @@ const dropdownStyles = {
noOptionsMessage: (provided: any) => ({
...provided,
whiteSpace: 'nowrap !important'
// minWidth: 'fit-content',
}),
container: (provided: any) => ({
...provided,
@ -93,12 +88,10 @@ const dropdownStyles = {
}),
singleValue: (provided: any, state: { isDisabled: any }) => {
const opacity = state.isDisabled ? 0.5 : 1;
const transition = 'opacity 300ms';
return {
...provided,
opacity,
transition,
transition: 'opacity 300ms',
display: 'flex',
alignItems: 'center',
height: '20px'
@ -187,30 +180,44 @@ const FilterAutoComplete: React.FC<Props> = ({
setQuery(value);
}, [value]);
const loadOptions = async (inputValue: string, callback: (options: { value: string; label: string }[]) => void) => {
if (!inputValue.length) {
const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value }));
setOptions(mappedValues);
callback(mappedValues);
setLoading(false);
return;
}
const loadOptions = useCallback(
async (inputValue: string, callback: (options: { value: string; label: string }[]) => void) => {
if (!inputValue.length) {
const mappedValues = topValues.map((i) => ({ value: i.value, label: i.value }));
setOptions(mappedValues);
callback(mappedValues);
setLoading(false);
return;
}
try {
// const response = await new APIClient()[method.toLowerCase()](endpoint, { ..._params, q: inputValue });
const data = await searchService.fetchAutoCompleteValues({ ..._params, q: inputValue })
// const data = await response.json();
const _options = data.map((i: any) => ({ value: i.value, label: i.value })) || [];
setOptions(_options);
callback(_options);
} catch (e) {
throw new Error(e);
} finally {
setLoading(false);
}
};
try {
const data = await searchService.fetchAutoCompleteValues({ ..._params, q: inputValue });
const _options = data.map((i: any) => ({ value: i.value, label: i.value })) || [];
setOptions(_options);
if (inputRef.current === document.activeElement) {
setMenuIsOpen(true);
}
callback(_options);
} catch (e) {
throw new Error(e);
} finally {
setLoading(false);
}
},
[topValues, _params]
);
const debouncedLoadOptions = useCallback(debounce(loadOptions, 1000), [params, topValues]);
const debouncedLoadOptions = useRef(
debounce((inputValue: string, callback: (options: { value: string; label: string }[]) => void) => {
loadOptions(inputValue, callback);
}, 1000)
).current;
useEffect(() => {
return () => {
debouncedLoadOptions.cancel && debouncedLoadOptions.cancel();
};
}, [debouncedLoadOptions]);
const handleInputChange = (newValue: string) => {
setLoading(true);

View file

@ -1,23 +1,11 @@
import React from 'react';
// import Select from 'Shared/Select';
import { Dropdown, MenuProps, Select, Space } from 'antd';
import { DownOutlined, SmileOutlined } from '@ant-design/icons';
import { MenuProps, Select } from 'antd';
interface Props {
payload: any;
}
function NodeDropdown(props: Props) {
const items: MenuProps['items'] = [
{
key: '1',
label: (
<a target='_blank' rel='noopener noreferrer' href='https://www.antgroup.com'>
1st menu item
</a>
)
}
];
return (
<Select style={{ width: 120 }} placeholder='Slect Event' dropdownStyle={{
border: 'none'

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