Merge remote-tracking branch 'origin/api-v1.5.5' into dev
This commit is contained in:
commit
7b9acc56ff
214 changed files with 34970 additions and 1760 deletions
1
.github/workflows/api.yaml
vendored
1
.github/workflows/api.yaml
vendored
|
|
@ -1,5 +1,6 @@
|
|||
# This action will push the chalice changes to aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- api-v1.5.5
|
||||
|
|
|
|||
1
.github/workflows/frontend.yaml
vendored
1
.github/workflows/frontend.yaml
vendored
|
|
@ -1,5 +1,6 @@
|
|||
name: Frontend FOSS Deployment
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ For those who want to simply use OpenReplay as a service, [sign up](https://app.
|
|||
|
||||
Please refer to the [official OpenReplay documentation](https://docs.openreplay.com/). That should help you troubleshoot common issues. For additional help, you can reach out to us on one of these channels:
|
||||
|
||||
- [Slack](https://slack.openreplay.com) (Connect with our engineers and community)
|
||||
- [Discord](https://discord.openreplay.com) (Connect with our engineers and community)
|
||||
- [GitHub](https://github.com/openreplay/openreplay/issues) (Bug and issue reports)
|
||||
- [Twitter](https://twitter.com/OpenReplayHQ) (Product updates, Great content)
|
||||
- [Website chat](https://openreplay.com) (Talk to us)
|
||||
|
|
@ -80,7 +80,7 @@ We're always on the lookout for contributions to OpenReplay, and we're glad you'
|
|||
|
||||
See our [Contributing Guide](CONTRIBUTING.md) for more details.
|
||||
|
||||
Also, feel free to join our [Slack](https://slack.openreplay.com) to ask questions, discuss ideas or connect with our contributors.
|
||||
Also, feel free to join our [Discord](https://discord.openreplay.com) to ask questions, discuss ideas or connect with our contributors.
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ pg_port=5432
|
|||
pg_user=postgres
|
||||
pg_timeout=30
|
||||
pg_minconn=45
|
||||
PG_RETRY_MAX=50
|
||||
PG_RETRY_INTERVAL=2
|
||||
put_S3_TTL=20
|
||||
sentryURL=
|
||||
sessions_bucket=mobs
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ WORKDIR /work
|
|||
COPY . .
|
||||
RUN pip install -r requirements.txt
|
||||
RUN mv .env.default .env
|
||||
ENV APP_NAME chalice
|
||||
|
||||
# Add Tini
|
||||
# Startup daemon
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ COPY . .
|
|||
RUN pip install -r requirements.txt
|
||||
RUN mv .env.default .env && mv app_alerts.py app.py
|
||||
ENV pg_minconn 2
|
||||
ENV APP_NAME alerts
|
||||
|
||||
# Add Tini
|
||||
# Startup daemon
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ from starlette.responses import StreamingResponse
|
|||
from chalicelib.utils import helper
|
||||
from chalicelib.utils import pg_client
|
||||
from routers import core, core_dynamic
|
||||
from routers.app import v1_api
|
||||
from routers.crons import core_crons
|
||||
from routers.crons import core_dynamic_crons
|
||||
from routers.subs import dashboard
|
||||
from routers.subs import dashboard, insights, metrics, v1_api
|
||||
|
||||
app = FastAPI()
|
||||
app = FastAPI(root_path="/api")
|
||||
|
||||
|
||||
@app.middleware('http')
|
||||
|
|
@ -54,7 +53,8 @@ app.include_router(core_dynamic.public_app)
|
|||
app.include_router(core_dynamic.app)
|
||||
app.include_router(core_dynamic.app_apikey)
|
||||
app.include_router(dashboard.app)
|
||||
# app.include_router(insights.app)
|
||||
app.include_router(metrics.app)
|
||||
app.include_router(insights.app)
|
||||
app.include_router(v1_api.app_apikey)
|
||||
|
||||
Schedule = AsyncIOScheduler()
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ def jwt_authorizer(token):
|
|||
try:
|
||||
payload = jwt.decode(
|
||||
token[1],
|
||||
config("jwt_secret"),
|
||||
"",
|
||||
algorithms=config("jwt_algorithm"),
|
||||
audience=[f"plugin:{helper.get_stage_name()}", f"front:{helper.get_stage_name()}"]
|
||||
audience=[ f"front:default-foss"]
|
||||
)
|
||||
except jwt.ExpiredSignatureError:
|
||||
print("! JWT Expired signature")
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ from chalicelib.utils.TimeUTC import TimeUTC
|
|||
PIE_CHART_GROUP = 5
|
||||
|
||||
|
||||
def __try_live(project_id, data: schemas.CreateCustomMetricsSchema):
|
||||
def __try_live(project_id, data: schemas.TryCustomMetricsPayloadSchema):
|
||||
results = []
|
||||
for i, s in enumerate(data.series):
|
||||
s.filter.startDate = data.startDate
|
||||
s.filter.endDate = data.endDate
|
||||
s.filter.startDate = data.startTimestamp
|
||||
s.filter.endDate = data.endTimestamp
|
||||
results.append(sessions.search2_series(data=s.filter, project_id=project_id, density=data.density,
|
||||
view_type=data.view_type, metric_type=data.metric_type,
|
||||
metric_of=data.metric_of, metric_value=data.metric_value))
|
||||
|
|
@ -42,7 +42,7 @@ def __try_live(project_id, data: schemas.CreateCustomMetricsSchema):
|
|||
return results
|
||||
|
||||
|
||||
def merged_live(project_id, data: schemas.CreateCustomMetricsSchema):
|
||||
def merged_live(project_id, data: schemas.TryCustomMetricsPayloadSchema):
|
||||
series_charts = __try_live(project_id=project_id, data=data)
|
||||
if data.view_type == schemas.MetricTimeseriesViewType.progress or data.metric_type == schemas.MetricType.table:
|
||||
return series_charts
|
||||
|
|
@ -54,13 +54,9 @@ def merged_live(project_id, data: schemas.CreateCustomMetricsSchema):
|
|||
return results
|
||||
|
||||
|
||||
def __get_merged_metric(project_id, user_id, metric_id,
|
||||
data: Union[schemas.CustomMetricChartPayloadSchema,
|
||||
schemas.CustomMetricSessionsPayloadSchema]) \
|
||||
def __merge_metric_with_data(metric, data: Union[schemas.CustomMetricChartPayloadSchema,
|
||||
schemas.CustomMetricSessionsPayloadSchema]) \
|
||||
-> Union[schemas.CreateCustomMetricsSchema, None]:
|
||||
metric = get(metric_id=metric_id, project_id=project_id, user_id=user_id, flatten=False)
|
||||
if metric is None:
|
||||
return None
|
||||
metric: schemas.CreateCustomMetricsSchema = schemas.CreateCustomMetricsSchema.parse_obj({**data.dict(), **metric})
|
||||
if len(data.filters) > 0 or len(data.events) > 0:
|
||||
for s in metric.series:
|
||||
|
|
@ -71,11 +67,12 @@ def __get_merged_metric(project_id, user_id, metric_id,
|
|||
return metric
|
||||
|
||||
|
||||
def make_chart(project_id, user_id, metric_id, data: schemas.CustomMetricChartPayloadSchema):
|
||||
metric: schemas.CreateCustomMetricsSchema = __get_merged_metric(project_id=project_id, user_id=user_id,
|
||||
metric_id=metric_id, data=data)
|
||||
def make_chart(project_id, user_id, metric_id, data: schemas.CustomMetricChartPayloadSchema, metric=None):
|
||||
if metric is None:
|
||||
metric = get(metric_id=metric_id, project_id=project_id, user_id=user_id, flatten=False)
|
||||
if metric is None:
|
||||
return None
|
||||
metric: schemas.CreateCustomMetricsSchema = __merge_metric_with_data(metric=metric, data=data)
|
||||
series_charts = __try_live(project_id=project_id, data=metric)
|
||||
if metric.view_type == schemas.MetricTimeseriesViewType.progress or metric.metric_type == schemas.MetricType.table:
|
||||
return series_charts
|
||||
|
|
@ -88,21 +85,23 @@ def make_chart(project_id, user_id, metric_id, data: schemas.CustomMetricChartPa
|
|||
|
||||
|
||||
def get_sessions(project_id, user_id, metric_id, data: schemas.CustomMetricSessionsPayloadSchema):
|
||||
metric: schemas.CreateCustomMetricsSchema = __get_merged_metric(project_id=project_id, user_id=user_id,
|
||||
metric_id=metric_id, data=data)
|
||||
metric = get(metric_id=metric_id, project_id=project_id, user_id=user_id, flatten=False)
|
||||
if metric is None:
|
||||
return None
|
||||
metric: schemas.CreateCustomMetricsSchema = __merge_metric_with_data(metric=metric, data=data)
|
||||
if metric is None:
|
||||
return None
|
||||
results = []
|
||||
for s in metric.series:
|
||||
s.filter.startDate = data.startDate
|
||||
s.filter.endDate = data.endDate
|
||||
s.filter.startDate = data.startTimestamp
|
||||
s.filter.endDate = data.endTimestamp
|
||||
results.append({"seriesId": s.series_id, "seriesName": s.name,
|
||||
**sessions.search2_pg(data=s.filter, project_id=project_id, user_id=user_id)})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def create(project_id, user_id, data: schemas.CreateCustomMetricsSchema):
|
||||
def create(project_id, user_id, data: schemas.CreateCustomMetricsSchema, dashboard=False):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
_data = {}
|
||||
for i, s in enumerate(data.series):
|
||||
|
|
@ -129,6 +128,8 @@ def create(project_id, user_id, data: schemas.CreateCustomMetricsSchema):
|
|||
query
|
||||
)
|
||||
r = cur.fetchone()
|
||||
if dashboard:
|
||||
return r["metric_id"]
|
||||
return {"data": get(metric_id=r["metric_id"], project_id=project_id, user_id=user_id)}
|
||||
|
||||
|
||||
|
|
@ -147,10 +148,11 @@ def update(metric_id, user_id, project_id, data: schemas.UpdateCustomMetricsSche
|
|||
"metric_value": data.metric_value, "metric_format": data.metric_format}
|
||||
for i, s in enumerate(data.series):
|
||||
prefix = "u_"
|
||||
if s.index is None:
|
||||
s.index = i
|
||||
if s.series_id is None or s.series_id not in series_ids:
|
||||
n_series.append({"i": i, "s": s})
|
||||
prefix = "n_"
|
||||
s.index = i
|
||||
else:
|
||||
u_series.append({"i": i, "s": s})
|
||||
u_series_ids.append(s.series_id)
|
||||
|
|
@ -192,40 +194,60 @@ def update(metric_id, user_id, project_id, data: schemas.UpdateCustomMetricsSche
|
|||
SET name = %(name)s, is_public= %(is_public)s,
|
||||
view_type= %(view_type)s, metric_type= %(metric_type)s,
|
||||
metric_of= %(metric_of)s, metric_value= %(metric_value)s,
|
||||
metric_format= %(metric_format)s
|
||||
metric_format= %(metric_format)s,
|
||||
edited_at = timezone('utc'::text, now())
|
||||
WHERE metric_id = %(metric_id)s
|
||||
AND project_id = %(project_id)s
|
||||
AND (user_id = %(user_id)s OR is_public)
|
||||
RETURNING metric_id;""", params)
|
||||
cur.execute(
|
||||
query
|
||||
)
|
||||
cur.execute(query)
|
||||
return get(metric_id=metric_id, project_id=project_id, user_id=user_id)
|
||||
|
||||
|
||||
def get_all(project_id, user_id):
|
||||
def get_all(project_id, user_id, include_series=False):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
cur.mogrify(
|
||||
"""SELECT *
|
||||
FROM metrics
|
||||
LEFT JOIN LATERAL (SELECT jsonb_agg(metric_series.* ORDER BY index) AS series
|
||||
sub_join = ""
|
||||
if include_series:
|
||||
sub_join = """LEFT JOIN LATERAL (SELECT COALESCE(jsonb_agg(metric_series.* ORDER BY index),'[]'::jsonb) AS series
|
||||
FROM metric_series
|
||||
WHERE metric_series.metric_id = metrics.metric_id
|
||||
AND metric_series.deleted_at ISNULL
|
||||
) AS metric_series ON (TRUE)
|
||||
) AS metric_series ON (TRUE)"""
|
||||
cur.execute(
|
||||
cur.mogrify(
|
||||
f"""SELECT *
|
||||
FROM metrics
|
||||
{sub_join}
|
||||
LEFT JOIN LATERAL (SELECT COALESCE(jsonb_agg(connected_dashboards.* ORDER BY is_public,name),'[]'::jsonb) AS dashboards
|
||||
FROM (SELECT DISTINCT dashboard_id, name, is_public
|
||||
FROM dashboards INNER JOIN dashboard_widgets USING (dashboard_id)
|
||||
WHERE deleted_at ISNULL
|
||||
AND dashboard_widgets.metric_id = metrics.metric_id
|
||||
AND project_id = %(project_id)s
|
||||
AND ((dashboards.user_id = %(user_id)s OR is_public))) AS connected_dashboards
|
||||
) AS connected_dashboards ON (TRUE)
|
||||
LEFT JOIN LATERAL (SELECT email AS owner_email
|
||||
FROM users
|
||||
WHERE deleted_at ISNULL
|
||||
AND users.user_id = metrics.user_id
|
||||
) AS owner ON (TRUE)
|
||||
WHERE metrics.project_id = %(project_id)s
|
||||
AND metrics.deleted_at ISNULL
|
||||
AND (user_id = %(user_id)s OR is_public)
|
||||
ORDER BY created_at;""",
|
||||
AND (user_id = %(user_id)s OR metrics.is_public)
|
||||
ORDER BY metrics.edited_at, metrics.created_at;""",
|
||||
{"project_id": project_id, "user_id": user_id}
|
||||
)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
for r in rows:
|
||||
r["created_at"] = TimeUTC.datetime_to_timestamp(r["created_at"])
|
||||
for s in r["series"]:
|
||||
s["filter"] = helper.old_search_payload_to_flat(s["filter"])
|
||||
if include_series:
|
||||
for r in rows:
|
||||
# r["created_at"] = TimeUTC.datetime_to_timestamp(r["created_at"])
|
||||
for s in r["series"]:
|
||||
s["filter"] = helper.old_search_payload_to_flat(s["filter"])
|
||||
else:
|
||||
for r in rows:
|
||||
r["created_at"] = TimeUTC.datetime_to_timestamp(r["created_at"])
|
||||
r["edited_at"] = TimeUTC.datetime_to_timestamp(r["edited_at"])
|
||||
rows = helper.list_to_camel_case(rows)
|
||||
return rows
|
||||
|
||||
|
|
@ -235,7 +257,7 @@ def delete(project_id, metric_id, user_id):
|
|||
cur.execute(
|
||||
cur.mogrify("""\
|
||||
UPDATE public.metrics
|
||||
SET deleted_at = timezone('utc'::text, now())
|
||||
SET deleted_at = timezone('utc'::text, now()), edited_at = timezone('utc'::text, now())
|
||||
WHERE project_id = %(project_id)s
|
||||
AND metric_id = %(metric_id)s
|
||||
AND (user_id = %(user_id)s OR is_public);""",
|
||||
|
|
@ -256,6 +278,18 @@ def get(metric_id, project_id, user_id, flatten=True):
|
|||
WHERE metric_series.metric_id = metrics.metric_id
|
||||
AND metric_series.deleted_at ISNULL
|
||||
) AS metric_series ON (TRUE)
|
||||
LEFT JOIN LATERAL (SELECT COALESCE(jsonb_agg(connected_dashboards.* ORDER BY is_public,name),'[]'::jsonb) AS dashboards
|
||||
FROM (SELECT dashboard_id, name, is_public
|
||||
FROM dashboards
|
||||
WHERE deleted_at ISNULL
|
||||
AND project_id = %(project_id)s
|
||||
AND ((user_id = %(user_id)s OR is_public))) AS connected_dashboards
|
||||
) AS connected_dashboards ON (TRUE)
|
||||
LEFT JOIN LATERAL (SELECT email AS owner_email
|
||||
FROM users
|
||||
WHERE deleted_at ISNULL
|
||||
AND users.user_id = metrics.user_id
|
||||
) AS owner ON (TRUE)
|
||||
WHERE metrics.project_id = %(project_id)s
|
||||
AND metrics.deleted_at ISNULL
|
||||
AND (metrics.user_id = %(user_id)s OR metrics.is_public)
|
||||
|
|
@ -268,12 +302,46 @@ def get(metric_id, project_id, user_id, flatten=True):
|
|||
if row is None:
|
||||
return None
|
||||
row["created_at"] = TimeUTC.datetime_to_timestamp(row["created_at"])
|
||||
row["edited_at"] = TimeUTC.datetime_to_timestamp(row["edited_at"])
|
||||
if flatten:
|
||||
for s in row["series"]:
|
||||
s["filter"] = helper.old_search_payload_to_flat(s["filter"])
|
||||
return helper.dict_to_camel_case(row)
|
||||
|
||||
|
||||
def get_with_template(metric_id, project_id, user_id, include_dashboard=True):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
sub_query = ""
|
||||
if include_dashboard:
|
||||
sub_query = """LEFT JOIN LATERAL (SELECT COALESCE(jsonb_agg(connected_dashboards.* ORDER BY is_public,name),'[]'::jsonb) AS dashboards
|
||||
FROM (SELECT dashboard_id, name, is_public
|
||||
FROM dashboards
|
||||
WHERE deleted_at ISNULL
|
||||
AND project_id = %(project_id)s
|
||||
AND ((user_id = %(user_id)s OR is_public))) AS connected_dashboards
|
||||
) AS connected_dashboards ON (TRUE)"""
|
||||
cur.execute(
|
||||
cur.mogrify(
|
||||
f"""SELECT *
|
||||
FROM metrics
|
||||
LEFT JOIN LATERAL (SELECT COALESCE(jsonb_agg(metric_series.* ORDER BY index),'[]'::jsonb) AS series
|
||||
FROM metric_series
|
||||
WHERE metric_series.metric_id = metrics.metric_id
|
||||
AND metric_series.deleted_at ISNULL
|
||||
) AS metric_series ON (TRUE)
|
||||
{sub_query}
|
||||
WHERE (metrics.project_id = %(project_id)s OR metrics.project_id ISNULL)
|
||||
AND metrics.deleted_at ISNULL
|
||||
AND (metrics.user_id = %(user_id)s OR metrics.is_public)
|
||||
AND metrics.metric_id = %(metric_id)s
|
||||
ORDER BY created_at;""",
|
||||
{"metric_id": metric_id, "project_id": project_id, "user_id": user_id}
|
||||
)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return helper.dict_to_camel_case(row)
|
||||
|
||||
|
||||
def get_series_for_alert(project_id, user_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
cur.execute(
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
309
api/chalicelib/core/dashboards2.py
Normal file
309
api/chalicelib/core/dashboards2.py
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
import json
|
||||
|
||||
import schemas
|
||||
from chalicelib.core import custom_metrics, dashboard
|
||||
from chalicelib.utils import helper
|
||||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
CATEGORY_DESCRIPTION = {
|
||||
'overview': 'lorem ipsum',
|
||||
'custom': 'lorem cusipsum',
|
||||
'errors': 'lorem erripsum',
|
||||
'performance': 'lorem perfipsum',
|
||||
'resources': 'lorem resipsum'
|
||||
}
|
||||
|
||||
|
||||
def get_templates(project_id, user_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = cur.mogrify(f"""SELECT category, jsonb_agg(metrics ORDER BY name) AS widgets
|
||||
FROM (SELECT * , default_config AS config
|
||||
FROM metrics LEFT JOIN LATERAL (SELECT COALESCE(jsonb_agg(metric_series.* ORDER BY index), '[]'::jsonb) AS series
|
||||
FROM metric_series
|
||||
WHERE metric_series.metric_id = metrics.metric_id
|
||||
AND metric_series.deleted_at ISNULL
|
||||
) AS metric_series ON (TRUE)
|
||||
WHERE deleted_at IS NULL
|
||||
AND (project_id ISNULL OR (project_id = %(project_id)s AND (is_public OR user_id= %(userId)s)))
|
||||
) AS metrics
|
||||
GROUP BY category
|
||||
ORDER BY category;""", {"project_id": project_id, "userId": user_id})
|
||||
cur.execute(pg_query)
|
||||
rows = cur.fetchall()
|
||||
for r in rows:
|
||||
r["description"] = CATEGORY_DESCRIPTION.get(r["category"], "")
|
||||
for w in r["widgets"]:
|
||||
w["created_at"] = TimeUTC.datetime_to_timestamp(w["created_at"])
|
||||
w["edited_at"] = TimeUTC.datetime_to_timestamp(w["edited_at"])
|
||||
return helper.list_to_camel_case(rows)
|
||||
|
||||
|
||||
def create_dashboard(project_id, user_id, data: schemas.CreateDashboardSchema):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = f"""INSERT INTO dashboards(project_id, user_id, name, is_public, is_pinned)
|
||||
VALUES(%(projectId)s, %(userId)s, %(name)s, %(is_public)s, %(is_pinned)s)
|
||||
RETURNING *"""
|
||||
params = {"userId": user_id, "projectId": project_id, **data.dict()}
|
||||
if data.metrics is not None and len(data.metrics) > 0:
|
||||
pg_query = f"""WITH dash AS ({pg_query})
|
||||
INSERT INTO dashboard_widgets(dashboard_id, metric_id, user_id, config)
|
||||
VALUES {",".join([f"((SELECT dashboard_id FROM dash),%(metric_id_{i})s, %(userId)s, (SELECT default_config FROM metrics WHERE metric_id=%(metric_id_{i})s)||%(config_{i})s)" for i in range(len(data.metrics))])}
|
||||
RETURNING (SELECT dashboard_id FROM dash)"""
|
||||
for i, m in enumerate(data.metrics):
|
||||
params[f"metric_id_{i}"] = m
|
||||
# params[f"config_{i}"] = schemas.AddWidgetToDashboardPayloadSchema.schema() \
|
||||
# .get("properties", {}).get("config", {}).get("default", {})
|
||||
# params[f"config_{i}"]["position"] = i
|
||||
# params[f"config_{i}"] = json.dumps(params[f"config_{i}"])
|
||||
params[f"config_{i}"] = json.dumps({"position": i})
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return {"errors": ["something went wrong while creating the dashboard"]}
|
||||
return {"data": get_dashboard(project_id=project_id, user_id=user_id, dashboard_id=row["dashboard_id"])}
|
||||
|
||||
|
||||
def get_dashboards(project_id, user_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = f"""SELECT *
|
||||
FROM dashboards
|
||||
WHERE deleted_at ISNULL
|
||||
AND project_id = %(projectId)s
|
||||
AND (user_id = %(userId)s OR is_public);"""
|
||||
params = {"userId": user_id, "projectId": project_id}
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
rows = cur.fetchall()
|
||||
return helper.list_to_camel_case(rows)
|
||||
|
||||
|
||||
def get_dashboard(project_id, user_id, dashboard_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = """SELECT dashboards.*, all_metric_widgets.widgets AS widgets
|
||||
FROM dashboards
|
||||
LEFT JOIN LATERAL (SELECT COALESCE(JSONB_AGG(raw_metrics), '[]') AS widgets
|
||||
FROM (SELECT dashboard_widgets.*, metrics.*, metric_series.series
|
||||
FROM metrics
|
||||
INNER JOIN dashboard_widgets USING (metric_id)
|
||||
LEFT JOIN LATERAL (SELECT JSONB_AGG(metric_series.* ORDER BY index) AS series
|
||||
FROM metric_series
|
||||
WHERE metric_series.metric_id = metrics.metric_id
|
||||
AND metric_series.deleted_at ISNULL
|
||||
) AS metric_series ON (TRUE)
|
||||
WHERE dashboard_widgets.dashboard_id = dashboards.dashboard_id
|
||||
AND metrics.deleted_at ISNULL
|
||||
AND (metrics.project_id = %(projectId)s OR metrics.project_id ISNULL)) AS raw_metrics
|
||||
) AS all_metric_widgets ON (TRUE)
|
||||
WHERE dashboards.deleted_at ISNULL
|
||||
AND dashboards.project_id = %(projectId)s
|
||||
AND dashboard_id = %(dashboard_id)s
|
||||
AND (dashboards.user_id = %(userId)s OR is_public);"""
|
||||
params = {"userId": user_id, "projectId": project_id, "dashboard_id": dashboard_id}
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
row = cur.fetchone()
|
||||
if row is not None:
|
||||
for w in row["widgets"]:
|
||||
row["created_at"] = TimeUTC.datetime_to_timestamp(w["created_at"])
|
||||
row["edited_at"] = TimeUTC.datetime_to_timestamp(w["edited_at"])
|
||||
return helper.dict_to_camel_case(row)
|
||||
|
||||
|
||||
def delete_dashboard(project_id, user_id, dashboard_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = """UPDATE dashboards
|
||||
SET deleted_at = timezone('utc'::text, now())
|
||||
WHERE dashboards.project_id = %(projectId)s
|
||||
AND dashboard_id = %(dashboard_id)s
|
||||
AND (dashboards.user_id = %(userId)s OR is_public);"""
|
||||
params = {"userId": user_id, "projectId": project_id, "dashboard_id": dashboard_id}
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
return {"data": {"success": True}}
|
||||
|
||||
|
||||
def update_dashboard(project_id, user_id, dashboard_id, data: schemas.EditDashboardSchema):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = f"""UPDATE dashboards
|
||||
SET name = %(name)s
|
||||
{", is_public = %(is_public)s" if data.is_public is not None else ""}
|
||||
{", is_pinned = %(is_pinned)s" if data.is_pinned is not None else ""}
|
||||
WHERE dashboards.project_id = %(projectId)s
|
||||
AND dashboard_id = %(dashboard_id)s
|
||||
AND (dashboards.user_id = %(userId)s OR is_public)"""
|
||||
params = {"userId": user_id, "projectId": project_id, "dashboard_id": dashboard_id, **data.dict()}
|
||||
if data.metrics is not None and len(data.metrics) > 0:
|
||||
pg_query = f"""WITH dash AS ({pg_query})
|
||||
INSERT INTO dashboard_widgets(dashboard_id, metric_id, user_id, config)
|
||||
VALUES {",".join([f"(%(dashboard_id)s, %(metric_id_{i})s, %(userId)s, (SELECT default_config FROM metrics WHERE metric_id=%(metric_id_{i})s)||%(config_{i})s)" for i in range(len(data.metrics))])};"""
|
||||
for i, m in enumerate(data.metrics):
|
||||
params[f"metric_id_{i}"] = m
|
||||
# params[f"config_{i}"] = schemas.AddWidgetToDashboardPayloadSchema.schema() \
|
||||
# .get("properties", {}).get("config", {}).get("default", {})
|
||||
# params[f"config_{i}"]["position"] = i
|
||||
# params[f"config_{i}"] = json.dumps(params[f"config_{i}"])
|
||||
params[f"config_{i}"] = json.dumps({"position": i})
|
||||
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
|
||||
return get_dashboard(project_id=project_id, user_id=user_id, dashboard_id=dashboard_id)
|
||||
|
||||
|
||||
def get_widget(project_id, user_id, dashboard_id, widget_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = """SELECT metrics.*, metric_series.series
|
||||
FROM dashboard_widgets
|
||||
INNER JOIN dashboards USING (dashboard_id)
|
||||
INNER JOIN metrics USING (metric_id)
|
||||
LEFT JOIN LATERAL (SELECT COALESCE(jsonb_agg(metric_series.* ORDER BY index), '[]'::jsonb) AS series
|
||||
FROM metric_series
|
||||
WHERE metric_series.metric_id = metrics.metric_id
|
||||
AND metric_series.deleted_at ISNULL
|
||||
) AS metric_series ON (TRUE)
|
||||
WHERE dashboard_id = %(dashboard_id)s
|
||||
AND widget_id = %(widget_id)s
|
||||
AND (dashboards.is_public OR dashboards.user_id = %(userId)s)
|
||||
AND dashboards.deleted_at IS NULL
|
||||
AND metrics.deleted_at ISNULL
|
||||
AND (metrics.project_id = %(projectId)s OR metrics.project_id ISNULL)
|
||||
AND (metrics.is_public OR metrics.user_id = %(userId)s);"""
|
||||
params = {"userId": user_id, "projectId": project_id, "dashboard_id": dashboard_id, "widget_id": widget_id}
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
row = cur.fetchone()
|
||||
return helper.dict_to_camel_case(row)
|
||||
|
||||
|
||||
def add_widget(project_id, user_id, dashboard_id, data: schemas.AddWidgetToDashboardPayloadSchema):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = """INSERT INTO dashboard_widgets(dashboard_id, metric_id, user_id, config)
|
||||
SELECT %(dashboard_id)s AS dashboard_id, %(metric_id)s AS metric_id,
|
||||
%(userId)s AS user_id, (SELECT default_config FROM metrics WHERE metric_id=%(metric_id)s)||%(config)s::jsonb AS config
|
||||
WHERE EXISTS(SELECT 1 FROM dashboards
|
||||
WHERE dashboards.deleted_at ISNULL AND dashboards.project_id = %(projectId)s
|
||||
AND dashboard_id = %(dashboard_id)s
|
||||
AND (dashboards.user_id = %(userId)s OR is_public))
|
||||
RETURNING *;"""
|
||||
params = {"userId": user_id, "projectId": project_id, "dashboard_id": dashboard_id, **data.dict()}
|
||||
params["config"] = json.dumps(data.config)
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
row = cur.fetchone()
|
||||
return helper.dict_to_camel_case(row)
|
||||
|
||||
|
||||
def update_widget(project_id, user_id, dashboard_id, widget_id, data: schemas.UpdateWidgetPayloadSchema):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = """UPDATE dashboard_widgets
|
||||
SET config= %(config)s
|
||||
WHERE dashboard_id=%(dashboard_id)s AND widget_id=%(widget_id)s
|
||||
RETURNING *;"""
|
||||
params = {"userId": user_id, "projectId": project_id, "dashboard_id": dashboard_id,
|
||||
"widget_id": widget_id, **data.dict()}
|
||||
params["config"] = json.dumps(data.config)
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
row = cur.fetchone()
|
||||
return helper.dict_to_camel_case(row)
|
||||
|
||||
|
||||
def remove_widget(project_id, user_id, dashboard_id, widget_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = """DELETE FROM dashboard_widgets
|
||||
WHERE dashboard_id=%(dashboard_id)s AND widget_id=%(widget_id)s;"""
|
||||
params = {"userId": user_id, "projectId": project_id, "dashboard_id": dashboard_id, "widget_id": widget_id}
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
return {"data": {"success": True}}
|
||||
|
||||
|
||||
def pin_dashboard(project_id, user_id, dashboard_id):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
pg_query = """UPDATE dashboards
|
||||
SET is_pinned = FALSE
|
||||
WHERE project_id=%(project_id)s;
|
||||
UPDATE dashboards
|
||||
SET is_pinned = True
|
||||
WHERE dashboard_id=%(dashboard_id)s AND project_id=%(project_id)s AND deleted_at ISNULL
|
||||
RETURNING *;"""
|
||||
params = {"userId": user_id, "project_id": project_id, "dashboard_id": dashboard_id}
|
||||
cur.execute(cur.mogrify(pg_query, params))
|
||||
row = cur.fetchone()
|
||||
return helper.dict_to_camel_case(row)
|
||||
|
||||
|
||||
def create_metric_add_widget(project_id, user_id, dashboard_id, data: schemas.CreateCustomMetricsSchema):
|
||||
metric_id = custom_metrics.create(project_id=project_id, user_id=user_id, data=data, dashboard=True)
|
||||
return add_widget(project_id=project_id, user_id=user_id, dashboard_id=dashboard_id,
|
||||
data=schemas.AddWidgetToDashboardPayloadSchema(metricId=metric_id))
|
||||
|
||||
|
||||
PREDEFINED = {schemas.TemplatePredefinedKeys.count_sessions: dashboard.get_processed_sessions,
|
||||
schemas.TemplatePredefinedKeys.avg_image_load_time: dashboard.get_application_activity_avg_image_load_time,
|
||||
schemas.TemplatePredefinedKeys.avg_page_load_time: dashboard.get_application_activity_avg_page_load_time,
|
||||
schemas.TemplatePredefinedKeys.avg_request_load_time: dashboard.get_application_activity_avg_request_load_time,
|
||||
schemas.TemplatePredefinedKeys.avg_dom_content_load_start: dashboard.get_page_metrics_avg_dom_content_load_start,
|
||||
schemas.TemplatePredefinedKeys.avg_first_contentful_pixel: dashboard.get_page_metrics_avg_first_contentful_pixel,
|
||||
schemas.TemplatePredefinedKeys.avg_visited_pages: dashboard.get_user_activity_avg_visited_pages,
|
||||
schemas.TemplatePredefinedKeys.avg_session_duration: dashboard.get_user_activity_avg_session_duration,
|
||||
schemas.TemplatePredefinedKeys.avg_pages_dom_buildtime: dashboard.get_pages_dom_build_time,
|
||||
schemas.TemplatePredefinedKeys.avg_pages_response_time: dashboard.get_pages_response_time,
|
||||
schemas.TemplatePredefinedKeys.avg_response_time: dashboard.get_top_metrics_avg_response_time,
|
||||
schemas.TemplatePredefinedKeys.avg_first_paint: dashboard.get_top_metrics_avg_first_paint,
|
||||
schemas.TemplatePredefinedKeys.avg_dom_content_loaded: dashboard.get_top_metrics_avg_dom_content_loaded,
|
||||
schemas.TemplatePredefinedKeys.avg_till_first_bit: dashboard.get_top_metrics_avg_till_first_bit,
|
||||
schemas.TemplatePredefinedKeys.avg_time_to_interactive: dashboard.get_top_metrics_avg_time_to_interactive,
|
||||
schemas.TemplatePredefinedKeys.count_requests: dashboard.get_top_metrics_count_requests,
|
||||
schemas.TemplatePredefinedKeys.avg_time_to_render: dashboard.get_time_to_render,
|
||||
schemas.TemplatePredefinedKeys.avg_used_js_heap_size: dashboard.get_memory_consumption,
|
||||
schemas.TemplatePredefinedKeys.avg_cpu: dashboard.get_avg_cpu,
|
||||
schemas.TemplatePredefinedKeys.avg_fps: dashboard.get_avg_fps,
|
||||
schemas.TemplatePredefinedKeys.impacted_sessions_by_js_errors: dashboard.get_impacted_sessions_by_js_errors,
|
||||
schemas.TemplatePredefinedKeys.domains_errors_4xx: dashboard.get_domains_errors_4xx,
|
||||
schemas.TemplatePredefinedKeys.domains_errors_5xx: dashboard.get_domains_errors_5xx,
|
||||
schemas.TemplatePredefinedKeys.errors_per_domains: dashboard.get_errors_per_domains,
|
||||
schemas.TemplatePredefinedKeys.calls_errors: dashboard.get_calls_errors,
|
||||
schemas.TemplatePredefinedKeys.errors_by_type: dashboard.get_errors_per_type,
|
||||
schemas.TemplatePredefinedKeys.errors_by_origin: dashboard.get_resources_by_party,
|
||||
schemas.TemplatePredefinedKeys.speed_index_by_location: dashboard.get_speed_index_location,
|
||||
schemas.TemplatePredefinedKeys.slowest_domains: dashboard.get_slowest_domains,
|
||||
schemas.TemplatePredefinedKeys.sessions_per_browser: dashboard.get_sessions_per_browser,
|
||||
schemas.TemplatePredefinedKeys.time_to_render: dashboard.get_time_to_render,
|
||||
schemas.TemplatePredefinedKeys.impacted_sessions_by_slow_pages: dashboard.get_impacted_sessions_by_slow_pages,
|
||||
schemas.TemplatePredefinedKeys.memory_consumption: dashboard.get_memory_consumption,
|
||||
schemas.TemplatePredefinedKeys.cpu_load: dashboard.get_avg_cpu,
|
||||
schemas.TemplatePredefinedKeys.frame_rate: dashboard.get_avg_fps,
|
||||
schemas.TemplatePredefinedKeys.crashes: dashboard.get_crashes,
|
||||
schemas.TemplatePredefinedKeys.resources_vs_visually_complete: dashboard.get_resources_vs_visually_complete,
|
||||
schemas.TemplatePredefinedKeys.pages_dom_buildtime: dashboard.get_pages_dom_build_time,
|
||||
schemas.TemplatePredefinedKeys.pages_response_time: dashboard.get_pages_response_time,
|
||||
schemas.TemplatePredefinedKeys.pages_response_time_distribution: dashboard.get_pages_response_time_distribution,
|
||||
schemas.TemplatePredefinedKeys.missing_resources: dashboard.get_missing_resources_trend,
|
||||
schemas.TemplatePredefinedKeys.slowest_resources: dashboard.get_slowest_resources,
|
||||
schemas.TemplatePredefinedKeys.resources_fetch_time: dashboard.get_resources_loading_time,
|
||||
schemas.TemplatePredefinedKeys.resource_type_vs_response_end: dashboard.resource_type_vs_response_end,
|
||||
schemas.TemplatePredefinedKeys.resources_count_by_type: dashboard.get_resources_count_by_type,
|
||||
}
|
||||
|
||||
|
||||
def get_predefined_metric(key: schemas.TemplatePredefinedKeys, project_id: int, data: dict):
|
||||
return PREDEFINED.get(key, lambda *args: None)(project_id=project_id, **data)
|
||||
|
||||
|
||||
def make_chart_metrics(project_id, user_id, metric_id, data: schemas.CustomMetricChartPayloadSchema):
|
||||
raw_metric = custom_metrics.get_with_template(metric_id=metric_id, project_id=project_id, user_id=user_id,
|
||||
include_dashboard=False)
|
||||
if raw_metric is None:
|
||||
return None
|
||||
metric = schemas.CustomMetricAndTemplate = schemas.CustomMetricAndTemplate.parse_obj(raw_metric)
|
||||
if metric.is_template:
|
||||
return get_predefined_metric(key=metric.predefined_key, project_id=project_id, data=data.dict())
|
||||
else:
|
||||
return custom_metrics.make_chart(project_id=project_id, user_id=user_id, metric_id=metric_id, data=data,
|
||||
metric=raw_metric)
|
||||
|
||||
|
||||
def make_chart_widget(dashboard_id, project_id, user_id, widget_id, data: schemas.CustomMetricChartPayloadSchema):
|
||||
raw_metric = get_widget(widget_id=widget_id, project_id=project_id, user_id=user_id, dashboard_id=dashboard_id)
|
||||
if raw_metric is None:
|
||||
return None
|
||||
metric = schemas.CustomMetricAndTemplate = schemas.CustomMetricAndTemplate.parse_obj(raw_metric)
|
||||
if metric.is_template:
|
||||
return get_predefined_metric(key=metric.predefined_key, project_id=project_id, data=data.dict())
|
||||
else:
|
||||
return custom_metrics.make_chart(project_id=project_id, user_id=user_id, metric_id=raw_metric["metricId"],
|
||||
data=data, metric=raw_metric)
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
import schemas
|
||||
from chalicelib.core import sessions_metas
|
||||
from chalicelib.core.dashboard import __get_constraints, __get_constraint_values
|
||||
from chalicelib.utils import helper, dev
|
||||
from chalicelib.utils import pg_client
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
from chalicelib.utils.metrics_helper import __get_step_size
|
||||
import math
|
||||
from chalicelib.core.dashboard import __get_constraints, __get_constraint_values
|
||||
|
||||
|
||||
def __transform_journey(rows):
|
||||
|
|
@ -930,4 +927,4 @@ def search(text, feature_type, project_id, platform=None):
|
|||
rows = cur.fetchall()
|
||||
else:
|
||||
return []
|
||||
return [helper.dict_to_camel_case(row) for row in rows]
|
||||
return [helper.dict_to_camel_case(row) for row in rows]
|
||||
|
|
|
|||
|
|
@ -15,10 +15,17 @@ class JIRAIntegration(integration_base.BaseIntegration):
|
|||
# TODO: enable super-constructor when OAuth is done
|
||||
# super(JIRAIntegration, self).__init__(jwt, user_id, JIRACloudIntegrationProxy)
|
||||
self._user_id = user_id
|
||||
i = self.get()
|
||||
if i is None:
|
||||
self.integration = self.get()
|
||||
if self.integration is None:
|
||||
return
|
||||
self.issue_handler = JIRACloudIntegrationIssue(token=i["token"], username=i["username"], url=i["url"])
|
||||
self.integration["valid"] = True
|
||||
try:
|
||||
self.issue_handler = JIRACloudIntegrationIssue(token=self.integration["token"],
|
||||
username=self.integration["username"],
|
||||
url=self.integration["url"])
|
||||
except Exception as e:
|
||||
self.issue_handler = None
|
||||
self.integration["valid"] = False
|
||||
|
||||
@property
|
||||
def provider(self):
|
||||
|
|
@ -37,10 +44,10 @@ class JIRAIntegration(integration_base.BaseIntegration):
|
|||
return helper.dict_to_camel_case(cur.fetchone())
|
||||
|
||||
def get_obfuscated(self):
|
||||
integration = self.get()
|
||||
if integration is None:
|
||||
if self.integration is None:
|
||||
return None
|
||||
integration["token"] = obfuscate_string(integration["token"])
|
||||
integration = dict(self.integration)
|
||||
integration["token"] = obfuscate_string(self.integration["token"])
|
||||
integration["provider"] = self.provider.lower()
|
||||
return integration
|
||||
|
||||
|
|
@ -90,14 +97,13 @@ class JIRAIntegration(integration_base.BaseIntegration):
|
|||
return {"state": "success"}
|
||||
|
||||
def add_edit(self, data):
|
||||
s = self.get()
|
||||
if s is not None:
|
||||
if self.integration is not None:
|
||||
return self.update(
|
||||
changes={
|
||||
"username": data["username"],
|
||||
"token": data["token"] \
|
||||
if data.get("token") and len(data["token"]) > 0 and data["token"].find("***") == -1 \
|
||||
else s["token"],
|
||||
else self.integration["token"],
|
||||
"url": data["url"]
|
||||
},
|
||||
obfuscate=True
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ def get_integration(tenant_id, user_id, tool=None):
|
|||
if tool not in SUPPORTED_TOOLS:
|
||||
return {"errors": [f"issue tracking tool not supported yet, available: {SUPPORTED_TOOLS}"]}, None
|
||||
if tool == integration_jira_cloud.PROVIDER:
|
||||
return None, integration_jira_cloud.JIRAIntegration(tenant_id=tenant_id, user_id=user_id)
|
||||
integration = integration_jira_cloud.JIRAIntegration(tenant_id=tenant_id, user_id=user_id)
|
||||
if integration.integration is not None and not integration.integration.get("valid", True):
|
||||
return {"errors": ["JIRA: connexion issue/unauthorized"]}, integration
|
||||
return None, integration
|
||||
elif tool == integration_github.PROVIDER:
|
||||
return None, integration_github.GitHubIntegration(tenant_id=tenant_id, user_id=user_id)
|
||||
return {"errors": ["lost integration"]}, None
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ def get_projects(tenant_id, recording_state=False, gdpr=None, recorded=False, st
|
|||
|
||||
cur.execute(f"""\
|
||||
SELECT
|
||||
s.project_id, s.name, s.project_key
|
||||
s.project_id, s.name, s.project_key, s.save_request_payloads
|
||||
{',s.gdpr' if gdpr else ''}
|
||||
{',COALESCE((SELECT TRUE FROM public.sessions WHERE sessions.project_id = s.project_id LIMIT 1), FALSE) AS recorded' if recorded else ''}
|
||||
{',stack_integrations.count>0 AS stack_integrations' if stack_integrations else ''}
|
||||
|
|
@ -65,27 +65,26 @@ def get_projects(tenant_id, recording_state=False, gdpr=None, recorded=False, st
|
|||
FROM public.projects AS s
|
||||
{'LEFT JOIN LATERAL (SELECT COUNT(*) AS count FROM public.integrations WHERE s.project_id = integrations.project_id LIMIT 1) AS stack_integrations ON TRUE' if stack_integrations else ''}
|
||||
WHERE s.deleted_at IS NULL
|
||||
ORDER BY s.project_id;"""
|
||||
)
|
||||
ORDER BY s.project_id;""")
|
||||
rows = cur.fetchall()
|
||||
if recording_state:
|
||||
project_ids = [f'({r["project_id"]})' for r in rows]
|
||||
query = f"""SELECT projects.project_id, COALESCE(MAX(start_ts), 0) AS last
|
||||
FROM (VALUES {",".join(project_ids)}) AS projects(project_id)
|
||||
LEFT JOIN sessions USING (project_id)
|
||||
GROUP BY project_id;"""
|
||||
cur.execute(
|
||||
query=query
|
||||
)
|
||||
query = cur.mogrify(f"""SELECT projects.project_id, COALESCE(MAX(start_ts), 0) AS last
|
||||
FROM (VALUES {",".join(project_ids)}) AS projects(project_id)
|
||||
LEFT JOIN sessions USING (project_id)
|
||||
WHERE sessions.start_ts >= %(startDate)s AND sessions.start_ts <= %(endDate)s
|
||||
GROUP BY project_id;""",
|
||||
{"startDate": TimeUTC.now(delta_days=-3), "endDate": TimeUTC.now(delta_days=1)})
|
||||
|
||||
cur.execute(query=query)
|
||||
status = cur.fetchall()
|
||||
for r in rows:
|
||||
r["status"] = "red"
|
||||
for s in status:
|
||||
if s["project_id"] == r["project_id"]:
|
||||
if s["last"] < TimeUTC.now(-2):
|
||||
r["status"] = "red"
|
||||
elif s["last"] < TimeUTC.now(-1):
|
||||
if TimeUTC.now(-2) <= s["last"] < TimeUTC.now(-1):
|
||||
r["status"] = "yellow"
|
||||
else:
|
||||
elif s["last"] >= TimeUTC.now(-1):
|
||||
r["status"] = "green"
|
||||
break
|
||||
|
||||
|
|
@ -109,7 +108,8 @@ def get_project(tenant_id, project_id, include_last_session=False, include_gdpr=
|
|||
SELECT
|
||||
s.project_id,
|
||||
s.project_key,
|
||||
s.name
|
||||
s.name,
|
||||
s.save_request_payloads
|
||||
{",(SELECT max(ss.start_ts) FROM public.sessions AS ss WHERE ss.project_id = %(project_id)s) AS last_recorded_session_at" if include_last_session else ""}
|
||||
{',s.gdpr' if include_gdpr else ''}
|
||||
{tracker_query}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ def __group_metadata(session, project_metadata):
|
|||
return meta
|
||||
|
||||
|
||||
def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_viewed=False, group_metadata=False):
|
||||
def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_viewed=False, group_metadata=False,
|
||||
live=True):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
extra_query = []
|
||||
if include_fav_viewed:
|
||||
|
|
@ -97,9 +98,9 @@ def get_by_id2_pg(project_id, session_id, user_id, full_data=False, include_fav_
|
|||
|
||||
data['metadata'] = __group_metadata(project_metadata=data.pop("projectMetadata"), session=data)
|
||||
data['issues'] = issues.get_by_session_id(session_id=session_id)
|
||||
data['live'] = assist.is_live(project_id=project_id,
|
||||
session_id=session_id,
|
||||
project_key=data["projectKey"])
|
||||
data['live'] = live and assist.is_live(project_id=project_id,
|
||||
session_id=session_id,
|
||||
project_key=data["projectKey"])
|
||||
data["inDB"] = True
|
||||
return data
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -88,13 +88,18 @@ class TimeUTC:
|
|||
return datetime.utcfromtimestamp(ts // 1000).strftime(fmt)
|
||||
|
||||
@staticmethod
|
||||
def human_to_timestamp(ts, pattern):
|
||||
def human_to_timestamp(ts, pattern="%Y-%m-%dT%H:%M:%S.%f"):
|
||||
return int(datetime.strptime(ts, pattern).timestamp() * 1000)
|
||||
|
||||
@staticmethod
|
||||
def datetime_to_timestamp(date):
|
||||
if date is None:
|
||||
return None
|
||||
if isinstance(date, str):
|
||||
fp = date.find(".")
|
||||
if fp > 0:
|
||||
date += '0' * (6 - len(date[fp + 1:]))
|
||||
date = datetime.fromisoformat(date)
|
||||
return int(datetime.timestamp(date) * 1000)
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -5,22 +5,24 @@ import requests
|
|||
from jira import JIRA
|
||||
from jira.exceptions import JIRAError
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from starlette import status
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
fields = "id, summary, description, creator, reporter, created, assignee, status, updated, comment, issuetype, labels"
|
||||
|
||||
|
||||
class JiraManager:
|
||||
# retries = 5
|
||||
retries = 0
|
||||
|
||||
def __init__(self, url, username, password, project_id=None):
|
||||
self._config = {"JIRA_PROJECT_ID": project_id, "JIRA_URL": url, "JIRA_USERNAME": username,
|
||||
"JIRA_PASSWORD": password}
|
||||
try:
|
||||
self._jira = JIRA({'server': url}, basic_auth=(username, password), logging=True, max_retries=1)
|
||||
self._jira = JIRA(url, basic_auth=(username, password), logging=True, max_retries=1)
|
||||
except Exception as e:
|
||||
print("!!! JIRA AUTH ERROR")
|
||||
print(e)
|
||||
raise e
|
||||
|
||||
def set_jira_project_id(self, project_id):
|
||||
self._config["JIRA_PROJECT_ID"] = project_id
|
||||
|
|
@ -33,8 +35,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.get_projects()
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
projects_dict_list = []
|
||||
for project in projects:
|
||||
projects_dict_list.append(self.__parser_project_info(project))
|
||||
|
|
@ -49,8 +51,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.get_project()
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
return self.__parser_project_info(project)
|
||||
|
||||
def get_issues(self, sql: str, offset: int = 0):
|
||||
|
|
@ -65,8 +67,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.get_issues(sql, offset)
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
|
||||
issue_dict_list = []
|
||||
for issue in issues:
|
||||
|
|
@ -85,8 +87,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.get_issue(issue_id)
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
return self.__parser_issue_info(issue)
|
||||
|
||||
def get_issue_v3(self, issue_id: str):
|
||||
|
|
@ -105,8 +107,8 @@ class JiraManager:
|
|||
if self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.get_issue_v3(issue_id)
|
||||
print(f"=>Error {e}")
|
||||
raise e
|
||||
print(f"=>Exception {e}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: get issue error")
|
||||
return self.__parser_issue_info(issue.json())
|
||||
|
||||
def create_issue(self, issue_dict):
|
||||
|
|
@ -119,8 +121,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.create_issue(issue_dict)
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
|
||||
def close_issue(self, issue):
|
||||
try:
|
||||
|
|
@ -131,8 +133,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.close_issue(issue)
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
|
||||
def assign_issue(self, issue_id, account_id) -> bool:
|
||||
try:
|
||||
|
|
@ -142,8 +144,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.assign_issue(issue_id, account_id)
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
|
||||
def add_comment(self, issue_id: str, comment: str):
|
||||
try:
|
||||
|
|
@ -153,8 +155,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.add_comment(issue_id, comment)
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
return self.__parser_comment_info(comment)
|
||||
|
||||
def add_comment_v3(self, issue_id: str, comment: str):
|
||||
|
|
@ -190,8 +192,8 @@ class JiraManager:
|
|||
if self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.add_comment_v3(issue_id, comment)
|
||||
print(f"=>Error {e}")
|
||||
raise e
|
||||
print(f"=>Exception {e}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: comment error")
|
||||
return self.__parser_comment_info(comment_response.json())
|
||||
|
||||
def get_comments(self, issueKey):
|
||||
|
|
@ -206,8 +208,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.get_comments(issueKey)
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
|
||||
def get_meta(self):
|
||||
meta = {}
|
||||
|
|
@ -217,14 +219,16 @@ class JiraManager:
|
|||
|
||||
def get_assignable_users(self):
|
||||
try:
|
||||
users = self._jira.search_assignable_users_for_issues('', project=self._config['JIRA_PROJECT_ID'])
|
||||
users = self._jira.search_assignable_users_for_issues(project=self._config['JIRA_PROJECT_ID'], query="*")
|
||||
except JIRAError as e:
|
||||
self.retries -= 1
|
||||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.get_assignable_users()
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
if e.status_code == 401:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="JIRA: 401 Unauthorized")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
users_dict = []
|
||||
for user in users:
|
||||
users_dict.append({
|
||||
|
|
@ -244,8 +248,8 @@ class JiraManager:
|
|||
if (e.status_code // 100) == 4 and self.retries > 0:
|
||||
time.sleep(1)
|
||||
return self.get_issue_types()
|
||||
print(f"=>Error {e.text}")
|
||||
raise e
|
||||
print(f"=>Exception {e.text}")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"JIRA: {e.text}")
|
||||
types_dict = []
|
||||
for type in types:
|
||||
if not type.subtask and not type.name.lower() == "epic":
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import time
|
||||
from threading import Semaphore
|
||||
|
||||
import psycopg2
|
||||
|
|
@ -9,7 +10,8 @@ _PG_CONFIG = {"host": config("pg_host"),
|
|||
"database": config("pg_dbname"),
|
||||
"user": config("pg_user"),
|
||||
"password": config("pg_password"),
|
||||
"port": config("pg_port", cast=int)}
|
||||
"port": config("pg_port", cast=int),
|
||||
"application_name": config("APP_NAME", default="PY")}
|
||||
PG_CONFIG = dict(_PG_CONFIG)
|
||||
if config("pg_timeout", cast=int, default=0) > 0:
|
||||
PG_CONFIG["options"] = f"-c statement_timeout={config('pg_timeout', cast=int) * 1000}"
|
||||
|
|
@ -36,9 +38,14 @@ class ORThreadedConnectionPool(psycopg2.pool.ThreadedConnectionPool):
|
|||
|
||||
postgreSQL_pool: ORThreadedConnectionPool = None
|
||||
|
||||
RETRY_MAX = config("PG_RETRY_MAX", cast=int, default=50)
|
||||
RETRY_INTERVAL = config("PG_RETRY_INTERVAL", cast=int, default=2)
|
||||
RETRY = 0
|
||||
|
||||
|
||||
def make_pool():
|
||||
global postgreSQL_pool
|
||||
global RETRY
|
||||
if postgreSQL_pool is not None:
|
||||
try:
|
||||
postgreSQL_pool.closeall()
|
||||
|
|
@ -50,7 +57,13 @@ def make_pool():
|
|||
print("Connection pool created successfully")
|
||||
except (Exception, psycopg2.DatabaseError) as error:
|
||||
print("Error while connecting to PostgreSQL", error)
|
||||
raise error
|
||||
if RETRY < RETRY_MAX:
|
||||
RETRY += 1
|
||||
print(f"waiting for {RETRY_INTERVAL}s before retry n°{RETRY}")
|
||||
time.sleep(RETRY_INTERVAL)
|
||||
make_pool()
|
||||
else:
|
||||
raise error
|
||||
|
||||
|
||||
make_pool()
|
||||
|
|
@ -64,6 +77,8 @@ class PostgresClient:
|
|||
def __init__(self, long_query=False):
|
||||
self.long_query = long_query
|
||||
if long_query:
|
||||
long_config = dict(_PG_CONFIG)
|
||||
long_config["application_name"] += "-LONG"
|
||||
self.connection = psycopg2.connect(**_PG_CONFIG)
|
||||
else:
|
||||
self.connection = postgreSQL_pool.getconn()
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ boto3==1.16.1
|
|||
pyjwt==1.7.1
|
||||
psycopg2-binary==2.8.6
|
||||
elasticsearch==7.9.1
|
||||
jira==2.0.0
|
||||
jira==3.1.1
|
||||
|
||||
|
||||
|
||||
fastapi==0.74.1
|
||||
fastapi==0.75.0
|
||||
uvicorn[standard]==0.17.5
|
||||
python-decouple==3.6
|
||||
pydantic[email]==1.8.2
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from routers.base import get_routers
|
|||
public_app, app, app_apikey = get_routers()
|
||||
|
||||
|
||||
@app.get('/{projectId}/sessions/{sessionId}', tags=["sessions"])
|
||||
@app.get('/{projectId}/sessions2/{sessionId}', tags=["sessions"])
|
||||
def get_session2(projectId: int, sessionId: Union[int, str], context: schemas.CurrentContext = Depends(OR_context)):
|
||||
if isinstance(sessionId, str):
|
||||
|
|
@ -36,6 +37,7 @@ def get_session2(projectId: int, sessionId: Union[int, str], context: schemas.Cu
|
|||
}
|
||||
|
||||
|
||||
@app.get('/{projectId}/sessions/{sessionId}/favorite', tags=["sessions"])
|
||||
@app.get('/{projectId}/sessions2/{sessionId}/favorite', tags=["sessions"])
|
||||
def add_remove_favorite_session2(projectId: int, sessionId: int,
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
|
|
@ -44,6 +46,7 @@ def add_remove_favorite_session2(projectId: int, sessionId: int,
|
|||
session_id=sessionId)}
|
||||
|
||||
|
||||
@app.get('/{projectId}/sessions/{sessionId}/assign', tags=["sessions"])
|
||||
@app.get('/{projectId}/sessions2/{sessionId}/assign', tags=["sessions"])
|
||||
def assign_session(projectId: int, sessionId, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = sessions_assignments.get_by_session(project_id=projectId, session_id=sessionId,
|
||||
|
|
@ -56,6 +59,7 @@ def assign_session(projectId: int, sessionId, context: schemas.CurrentContext =
|
|||
}
|
||||
|
||||
|
||||
@app.get('/{projectId}/sessions/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"])
|
||||
@app.get('/{projectId}/sessions2/{sessionId}/errors/{errorId}/sourcemaps', tags=["sessions", "sourcemaps"])
|
||||
def get_error_trace(projectId: int, sessionId: int, errorId: str,
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
|
|
@ -67,6 +71,7 @@ def get_error_trace(projectId: int, sessionId: int, errorId: str,
|
|||
}
|
||||
|
||||
|
||||
@app.get('/{projectId}/sessions/{sessionId}/assign/{issueId}', tags=["sessions", "issueTracking"])
|
||||
@app.get('/{projectId}/sessions2/{sessionId}/assign/{issueId}', tags=["sessions", "issueTracking"])
|
||||
def assign_session(projectId: int, sessionId: int, issueId: str,
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
|
|
@ -79,6 +84,8 @@ def assign_session(projectId: int, sessionId: int, issueId: str,
|
|||
}
|
||||
|
||||
|
||||
@app.post('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"])
|
||||
@app.put('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"])
|
||||
@app.post('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"])
|
||||
@app.put('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"])
|
||||
def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schemas.CommentAssignmentSchema = Body(...),
|
||||
|
|
@ -387,7 +394,7 @@ def delete_sumologic(projectId: int, context: schemas.CurrentContext = Depends(O
|
|||
def get_integration_status(context: schemas.CurrentContext = Depends(OR_context)):
|
||||
error, integration = integrations_manager.get_integration(tenant_id=context.tenant_id,
|
||||
user_id=context.user_id)
|
||||
if error is not None:
|
||||
if error is not None and integration is None:
|
||||
return {"data": {}}
|
||||
return {"data": integration.get_obfuscated()}
|
||||
|
||||
|
|
@ -399,7 +406,7 @@ def add_edit_jira_cloud(data: schemas.JiraGithubSchema = Body(...),
|
|||
error, integration = integrations_manager.get_integration(tool=integration_jira_cloud.PROVIDER,
|
||||
tenant_id=context.tenant_id,
|
||||
user_id=context.user_id)
|
||||
if error is not None:
|
||||
if error is not None and integration is None:
|
||||
return error
|
||||
data.provider = integration_jira_cloud.PROVIDER
|
||||
return {"data": integration.add_edit(data=data.dict())}
|
||||
|
|
@ -422,7 +429,7 @@ def add_edit_github(data: schemas.JiraGithubSchema = Body(...),
|
|||
def delete_default_issue_tracking_tool(context: schemas.CurrentContext = Depends(OR_context)):
|
||||
error, integration = integrations_manager.get_integration(tenant_id=context.tenant_id,
|
||||
user_id=context.user_id)
|
||||
if error is not None:
|
||||
if error is not None and integration is None:
|
||||
return error
|
||||
return {"data": integration.delete()}
|
||||
|
||||
|
|
@ -825,6 +832,19 @@ def sessions_live(projectId: int, userId: str = None, context: schemas.CurrentCo
|
|||
return {'data': data}
|
||||
|
||||
|
||||
@app.get('/{projectId}/assist/sessions/{sessionId}', tags=["assist"])
|
||||
def get_live_session(projectId: int, sessionId: str, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = assist.get_live_session_by_id(project_id=projectId, session_id=sessionId)
|
||||
if data is None:
|
||||
data = sessions.get_by_id2_pg(project_id=projectId, session_id=sessionId, full_data=True,
|
||||
user_id=context.user_id, include_fav_viewed=True, group_metadata=True, live=False)
|
||||
if data is None:
|
||||
return {"errors": ["session not found"]}
|
||||
if data.get("inDB"):
|
||||
sessions_favorite_viewed.view_session(project_id=projectId, user_id=context.user_id, session_id=sessionId)
|
||||
return {'data': data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/heatmaps/url', tags=["heatmaps"])
|
||||
def get_heatmaps_by_url(projectId: int, data: schemas.GetHeatmapPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
|
|
@ -1065,78 +1085,6 @@ def change_client_password(data: schemas.EditUserPasswordSchema = Body(...),
|
|||
user_id=context.user_id)
|
||||
|
||||
|
||||
@app.post('/{projectId}/custom_metrics/try', tags=["customMetrics"])
|
||||
@app.put('/{projectId}/custom_metrics/try', tags=["customMetrics"])
|
||||
def try_custom_metric(projectId: int, data: schemas.CreateCustomMetricsSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": custom_metrics.merged_live(project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.post('/{projectId}/custom_metrics', tags=["customMetrics"])
|
||||
@app.put('/{projectId}/custom_metrics', tags=["customMetrics"])
|
||||
def add_custom_metric(projectId: int, data: schemas.CreateCustomMetricsSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return custom_metrics.create(project_id=projectId, user_id=context.user_id, data=data)
|
||||
|
||||
|
||||
@app.get('/{projectId}/custom_metrics', tags=["customMetrics"])
|
||||
def get_custom_metrics(projectId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": custom_metrics.get_all(project_id=projectId, user_id=context.user_id)}
|
||||
|
||||
|
||||
@app.get('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"])
|
||||
def get_custom_metric(projectId: int, metric_id: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = custom_metrics.get(project_id=projectId, user_id=context.user_id, metric_id=metric_id)
|
||||
if data is None:
|
||||
return {"errors": ["custom metric not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/custom_metrics/{metric_id}/sessions', tags=["customMetrics"])
|
||||
def get_custom_metric_sessions(projectId: int, metric_id: int,
|
||||
data: schemas.CustomMetricSessionsPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = custom_metrics.get_sessions(project_id=projectId, user_id=context.user_id, metric_id=metric_id, data=data)
|
||||
if data is None:
|
||||
return {"errors": ["custom metric not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/custom_metrics/{metric_id}/chart', tags=["customMetrics"])
|
||||
def get_custom_metric_chart(projectId: int, metric_id: int, data: schemas.CustomMetricChartPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = custom_metrics.make_chart(project_id=projectId, user_id=context.user_id, metric_id=metric_id,
|
||||
data=data)
|
||||
if data is None:
|
||||
return {"errors": ["custom metric not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"])
|
||||
@app.put('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"])
|
||||
def update_custom_metric(projectId: int, metric_id: int, data: schemas.UpdateCustomMetricsSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = custom_metrics.update(project_id=projectId, user_id=context.user_id, metric_id=metric_id, data=data)
|
||||
if data is None:
|
||||
return {"errors": ["custom metric not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/custom_metrics/{metric_id}/status', tags=["customMetrics"])
|
||||
@app.put('/{projectId}/custom_metrics/{metric_id}/status', tags=["customMetrics"])
|
||||
def update_custom_metric_state(projectId: int, metric_id: int,
|
||||
data: schemas.UpdateCustomMetricsStatusSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {
|
||||
"data": custom_metrics.change_state(project_id=projectId, user_id=context.user_id, metric_id=metric_id,
|
||||
status=data.active)}
|
||||
|
||||
|
||||
@app.delete('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"])
|
||||
def delete_custom_metric(projectId: int, metric_id: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": custom_metrics.delete(project_id=projectId, user_id=context.user_id, metric_id=metric_id)}
|
||||
|
||||
|
||||
@app.post('/{projectId}/saved_search', tags=["savedSearch"])
|
||||
@app.put('/{projectId}/saved_search', tags=["savedSearch"])
|
||||
def add_saved_search(projectId: int, data: schemas.SavedSearchSchema = Body(...),
|
||||
|
|
|
|||
|
|
@ -325,22 +325,73 @@ def get_dashboard_resources_count_by_type(projectId: int, data: schemas.MetricPa
|
|||
@app.post('/{projectId}/dashboard/overview', tags=["dashboard", "metrics"])
|
||||
@app.get('/{projectId}/dashboard/overview', tags=["dashboard", "metrics"])
|
||||
def get_dashboard_group(projectId: int, data: schemas.MetricPayloadSchema = Body(...)):
|
||||
return {"data": [
|
||||
*helper.explode_widget(key="count_sessions",
|
||||
data=dashboard.get_processed_sessions(project_id=projectId, **data.dict())),
|
||||
results = [
|
||||
{"key": "count_sessions",
|
||||
"data": dashboard.get_processed_sessions(project_id=projectId, **data.dict())},
|
||||
*helper.explode_widget(data={**dashboard.get_application_activity(project_id=projectId, **data.dict()),
|
||||
"chart": dashboard.get_performance(project_id=projectId, **data.dict())
|
||||
.get("chart", [])}),
|
||||
*helper.explode_widget(data=dashboard.get_page_metrics(project_id=projectId, **data.dict())),
|
||||
*helper.explode_widget(data=dashboard.get_user_activity(project_id=projectId, **data.dict())),
|
||||
*helper.explode_widget(data=dashboard.get_pages_dom_build_time(project_id=projectId, **data.dict()),
|
||||
key="avg_pages_dom_buildtime"),
|
||||
*helper.explode_widget(data=dashboard.get_pages_response_time(project_id=projectId, **data.dict()),
|
||||
key="avg_pages_response_time"),
|
||||
{"key": "avg_pages_dom_buildtime",
|
||||
"data": dashboard.get_pages_dom_build_time(project_id=projectId, **data.dict())},
|
||||
{"key": "avg_pages_response_time",
|
||||
"data": dashboard.get_pages_response_time(project_id=projectId, **data.dict())
|
||||
},
|
||||
*helper.explode_widget(dashboard.get_top_metrics(project_id=projectId, **data.dict())),
|
||||
*helper.explode_widget(data=dashboard.get_time_to_render(project_id=projectId, **data.dict()),
|
||||
key="avg_time_to_render"),
|
||||
*helper.explode_widget(dashboard.get_memory_consumption(project_id=projectId, **data.dict())),
|
||||
*helper.explode_widget(dashboard.get_avg_cpu(project_id=projectId, **data.dict())),
|
||||
*helper.explode_widget(dashboard.get_avg_fps(project_id=projectId, **data.dict())),
|
||||
]}
|
||||
{"key": "avg_time_to_render", "data": dashboard.get_time_to_render(project_id=projectId, **data.dict())},
|
||||
{"key": "avg_used_js_heap_size", "data": dashboard.get_memory_consumption(project_id=projectId, **data.dict())},
|
||||
{"key": "avg_cpu", "data": dashboard.get_avg_cpu(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_fps, "data": dashboard.get_avg_fps(project_id=projectId, **data.dict())}
|
||||
]
|
||||
results = sorted(results, key=lambda r: r["key"])
|
||||
return {"data": results}
|
||||
|
||||
|
||||
@app.post('/{projectId}/dashboard/overview2', tags=["dashboard", "metrics"])
|
||||
@app.get('/{projectId}/dashboard/overview2', tags=["dashboard", "metrics"])
|
||||
def get_dashboard_group(projectId: int, data: schemas.MetricPayloadSchema = Body(...)):
|
||||
results = [
|
||||
{"key": schemas.TemplatePredefinedKeys.count_sessions,
|
||||
"data": dashboard.get_processed_sessions(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_image_load_time,
|
||||
"data": dashboard.get_application_activity_avg_image_load_time(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_page_load_time,
|
||||
"data": dashboard.get_application_activity_avg_page_load_time(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_request_load_time,
|
||||
"data": dashboard.get_application_activity_avg_request_load_time(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_dom_content_load_start,
|
||||
"data": dashboard.get_page_metrics_avg_dom_content_load_start(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_first_contentful_pixel,
|
||||
"data": dashboard.get_page_metrics_avg_first_contentful_pixel(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_visited_pages,
|
||||
"data": dashboard.get_user_activity_avg_visited_pages(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_session_duration,
|
||||
"data": dashboard.get_user_activity_avg_session_duration(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_pages_dom_buildtime,
|
||||
"data": dashboard.get_pages_dom_build_time(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_pages_response_time,
|
||||
"data": dashboard.get_pages_response_time(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_response_time,
|
||||
"data": dashboard.get_top_metrics_avg_response_time(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_first_paint,
|
||||
"data": dashboard.get_top_metrics_avg_first_paint(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_dom_content_loaded,
|
||||
"data": dashboard.get_top_metrics_avg_dom_content_loaded(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_till_first_bit,
|
||||
"data": dashboard.get_top_metrics_avg_till_first_bit(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_time_to_interactive,
|
||||
"data": dashboard.get_top_metrics_avg_time_to_interactive(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.count_requests,
|
||||
"data": dashboard.get_top_metrics_count_requests(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_time_to_render,
|
||||
"data": dashboard.get_time_to_render(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_used_js_heap_size,
|
||||
"data": dashboard.get_memory_consumption(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_cpu,
|
||||
"data": dashboard.get_avg_cpu(project_id=projectId, **data.dict())},
|
||||
{"key": schemas.TemplatePredefinedKeys.avg_fps,
|
||||
"data": dashboard.get_avg_fps(project_id=projectId, **data.dict())}
|
||||
]
|
||||
results = sorted(results, key=lambda r: r["key"])
|
||||
return {"data": results}
|
||||
|
|
|
|||
181
api/routers/subs/metrics.py
Normal file
181
api/routers/subs/metrics.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
from fastapi import Body, Depends
|
||||
|
||||
import schemas
|
||||
from chalicelib.core import dashboards2, custom_metrics
|
||||
from or_dependencies import OR_context
|
||||
from routers.base import get_routers
|
||||
|
||||
public_app, app, app_apikey = get_routers()
|
||||
|
||||
|
||||
@app.post('/{projectId}/dashboards', tags=["dashboard"])
|
||||
@app.put('/{projectId}/dashboards', tags=["dashboard"])
|
||||
def create_dashboards(projectId: int, data: schemas.CreateDashboardSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return dashboards2.create_dashboard(project_id=projectId, user_id=context.user_id, data=data)
|
||||
|
||||
|
||||
@app.get('/{projectId}/dashboards', tags=["dashboard"])
|
||||
def get_dashboards(projectId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": dashboards2.get_dashboards(project_id=projectId, user_id=context.user_id)}
|
||||
|
||||
|
||||
@app.get('/{projectId}/dashboards/{dashboardId}', tags=["dashboard"])
|
||||
def get_dashboard(projectId: int, dashboardId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = dashboards2.get_dashboard(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId)
|
||||
if data is None:
|
||||
return {"errors": ["dashboard not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/dashboards/{dashboardId}', tags=["dashboard"])
|
||||
@app.put('/{projectId}/dashboards/{dashboardId}', tags=["dashboard"])
|
||||
def update_dashboard(projectId: int, dashboardId: int, data: schemas.EditDashboardSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": dashboards2.update_dashboard(project_id=projectId, user_id=context.user_id,
|
||||
dashboard_id=dashboardId, data=data)}
|
||||
|
||||
|
||||
@app.delete('/{projectId}/dashboards/{dashboardId}', tags=["dashboard"])
|
||||
def delete_dashboard(projectId: int, dashboardId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return dashboards2.delete_dashboard(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId)
|
||||
|
||||
|
||||
@app.get('/{projectId}/dashboards/{dashboardId}/pin', tags=["dashboard"])
|
||||
def pin_dashboard(projectId: int, dashboardId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": dashboards2.pin_dashboard(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId)}
|
||||
|
||||
|
||||
@app.post('/{projectId}/dashboards/{dashboardId}/widgets', tags=["dashboard"])
|
||||
@app.put('/{projectId}/dashboards/{dashboardId}/widgets', tags=["dashboard"])
|
||||
def add_widget_to_dashboard(projectId: int, dashboardId: int,
|
||||
data: schemas.AddWidgetToDashboardPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": dashboards2.add_widget(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId,
|
||||
data=data)}
|
||||
|
||||
|
||||
@app.post('/{projectId}/dashboards/{dashboardId}/metrics', tags=["dashboard"])
|
||||
@app.put('/{projectId}/dashboards/{dashboardId}/metrics', tags=["dashboard"])
|
||||
def create_metric_and_add_to_dashboard(projectId: int, dashboardId: int,
|
||||
data: schemas.CreateCustomMetricsSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": dashboards2.create_metric_add_widget(project_id=projectId, user_id=context.user_id,
|
||||
dashboard_id=dashboardId, data=data)}
|
||||
|
||||
|
||||
@app.post('/{projectId}/dashboards/{dashboardId}/widgets/{widgetId}', tags=["dashboard"])
|
||||
@app.put('/{projectId}/dashboards/{dashboardId}/widgets/{widgetId}', tags=["dashboard"])
|
||||
def update_widget_in_dashboard(projectId: int, dashboardId: int, widgetId: int,
|
||||
data: schemas.UpdateWidgetPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return dashboards2.update_widget(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId,
|
||||
widget_id=widgetId, data=data)
|
||||
|
||||
|
||||
@app.delete('/{projectId}/dashboards/{dashboardId}/widgets/{widgetId}', tags=["dashboard"])
|
||||
def remove_widget_from_dashboard(projectId: int, dashboardId: int, widgetId: int,
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return dashboards2.remove_widget(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId,
|
||||
widget_id=widgetId)
|
||||
|
||||
|
||||
@app.post('/{projectId}/dashboards/{dashboardId}/widgets/{widgetId}/chart', tags=["dashboard"])
|
||||
def get_widget_chart(projectId: int, dashboardId: int, widgetId: int,
|
||||
data: schemas.CustomMetricChartPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = dashboards2.make_chart_widget(project_id=projectId, user_id=context.user_id, dashboard_id=dashboardId,
|
||||
widget_id=widgetId, data=data)
|
||||
if data is None:
|
||||
return {"errors": ["widget not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.get('/{projectId}/metrics/templates', tags=["dashboard"])
|
||||
def get_templates(projectId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": dashboards2.get_templates(project_id=projectId, user_id=context.user_id)}
|
||||
|
||||
|
||||
@app.post('/{projectId}/metrics/try', tags=["dashboard"])
|
||||
@app.put('/{projectId}/metrics/try', tags=["dashboard"])
|
||||
@app.post('/{projectId}/custom_metrics/try', tags=["customMetrics"])
|
||||
@app.put('/{projectId}/custom_metrics/try', tags=["customMetrics"])
|
||||
def try_custom_metric(projectId: int, data: schemas.TryCustomMetricsPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": custom_metrics.merged_live(project_id=projectId, data=data)}
|
||||
|
||||
|
||||
@app.post('/{projectId}/metrics', tags=["dashboard"])
|
||||
@app.put('/{projectId}/metrics', tags=["dashboard"])
|
||||
@app.post('/{projectId}/custom_metrics', tags=["customMetrics"])
|
||||
@app.put('/{projectId}/custom_metrics', tags=["customMetrics"])
|
||||
def add_custom_metric(projectId: int, data: schemas.CreateCustomMetricsSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return custom_metrics.create(project_id=projectId, user_id=context.user_id, data=data)
|
||||
|
||||
|
||||
@app.get('/{projectId}/metrics', tags=["dashboard"])
|
||||
@app.get('/{projectId}/custom_metrics', tags=["customMetrics"])
|
||||
def get_custom_metrics(projectId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": custom_metrics.get_all(project_id=projectId, user_id=context.user_id)}
|
||||
|
||||
|
||||
@app.get('/{projectId}/metrics/{metric_id}', tags=["dashboard"])
|
||||
@app.get('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"])
|
||||
def get_custom_metric(projectId: int, metric_id: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = custom_metrics.get(project_id=projectId, user_id=context.user_id, metric_id=metric_id)
|
||||
if data is None:
|
||||
return {"errors": ["custom metric not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/metrics/{metric_id}/sessions', tags=["dashboard"])
|
||||
@app.post('/{projectId}/custom_metrics/{metric_id}/sessions', tags=["customMetrics"])
|
||||
def get_custom_metric_sessions(projectId: int, metric_id: int,
|
||||
data: schemas.CustomMetricSessionsPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = custom_metrics.get_sessions(project_id=projectId, user_id=context.user_id, metric_id=metric_id, data=data)
|
||||
if data is None:
|
||||
return {"errors": ["custom metric not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/metrics/{metric_id}/chart', tags=["dashboard"])
|
||||
@app.post('/{projectId}/custom_metrics/{metric_id}/chart', tags=["customMetrics"])
|
||||
def get_custom_metric_chart(projectId: int, metric_id: int, data: schemas.CustomMetricChartPayloadSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = dashboards2.make_chart_metrics(project_id=projectId, user_id=context.user_id, metric_id=metric_id,
|
||||
data=data)
|
||||
if data is None:
|
||||
return {"errors": ["custom metric not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/metrics/{metric_id}', tags=["dashboard"])
|
||||
@app.put('/{projectId}/metrics/{metric_id}', tags=["dashboard"])
|
||||
@app.post('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"])
|
||||
@app.put('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"])
|
||||
def update_custom_metric(projectId: int, metric_id: int, data: schemas.UpdateCustomMetricsSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = custom_metrics.update(project_id=projectId, user_id=context.user_id, metric_id=metric_id, data=data)
|
||||
if data is None:
|
||||
return {"errors": ["custom metric not found"]}
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@app.post('/{projectId}/metrics/{metric_id}/status', tags=["dashboard"])
|
||||
@app.put('/{projectId}/metrics/{metric_id}/status', tags=["dashboard"])
|
||||
@app.post('/{projectId}/custom_metrics/{metric_id}/status', tags=["customMetrics"])
|
||||
@app.put('/{projectId}/custom_metrics/{metric_id}/status', tags=["customMetrics"])
|
||||
def update_custom_metric_state(projectId: int, metric_id: int,
|
||||
data: schemas.UpdateCustomMetricsStatusSchema = Body(...),
|
||||
context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {
|
||||
"data": custom_metrics.change_state(project_id=projectId, user_id=context.user_id, metric_id=metric_id,
|
||||
status=data.active)}
|
||||
|
||||
|
||||
@app.delete('/{projectId}/metrics/{metric_id}', tags=["dashboard"])
|
||||
@app.delete('/{projectId}/custom_metrics/{metric_id}', tags=["customMetrics"])
|
||||
def delete_custom_metric(projectId: int, metric_id: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
return {"data": custom_metrics.delete(project_id=projectId, user_id=context.user_id, metric_id=metric_id)}
|
||||
111
api/schemas.py
111
api/schemas.py
|
|
@ -776,6 +776,7 @@ class CustomMetricCreateSeriesSchema(BaseModel):
|
|||
class MetricTimeseriesViewType(str, Enum):
|
||||
line_chart = "lineChart"
|
||||
progress = "progress"
|
||||
area_chart = "areaChart"
|
||||
|
||||
|
||||
class MetricTableViewType(str, Enum):
|
||||
|
|
@ -803,8 +804,8 @@ class TimeseriesMetricOfType(str, Enum):
|
|||
|
||||
|
||||
class CustomMetricSessionsPayloadSchema(FlatSessionsSearch):
|
||||
startDate: int = Field(TimeUTC.now(-7))
|
||||
endDate: int = Field(TimeUTC.now())
|
||||
startTimestamp: int = Field(TimeUTC.now(-7))
|
||||
endTimestamp: int = Field(TimeUTC.now())
|
||||
|
||||
class Config:
|
||||
alias_generator = attribute_to_camel_case
|
||||
|
|
@ -817,10 +818,10 @@ class CustomMetricChartPayloadSchema(CustomMetricSessionsPayloadSchema):
|
|||
alias_generator = attribute_to_camel_case
|
||||
|
||||
|
||||
class CreateCustomMetricsSchema(CustomMetricChartPayloadSchema):
|
||||
class TryCustomMetricsPayloadSchema(CustomMetricChartPayloadSchema):
|
||||
name: str = Field(...)
|
||||
series: List[CustomMetricCreateSeriesSchema] = Field(..., min_items=1)
|
||||
is_public: bool = Field(default=True, const=True)
|
||||
series: List[CustomMetricCreateSeriesSchema] = Field(...)
|
||||
is_public: bool = Field(default=True)
|
||||
view_type: Union[MetricTimeseriesViewType, MetricTableViewType] = Field(MetricTimeseriesViewType.line_chart)
|
||||
metric_type: MetricType = Field(MetricType.timeseries)
|
||||
metric_of: Union[TableMetricOfType, TimeseriesMetricOfType] = Field(TableMetricOfType.user_id)
|
||||
|
|
@ -858,6 +859,10 @@ class CreateCustomMetricsSchema(CustomMetricChartPayloadSchema):
|
|||
alias_generator = attribute_to_camel_case
|
||||
|
||||
|
||||
class CreateCustomMetricsSchema(TryCustomMetricsPayloadSchema):
|
||||
series: List[CustomMetricCreateSeriesSchema] = Field(..., min_items=1)
|
||||
|
||||
|
||||
class CustomMetricUpdateSeriesSchema(CustomMetricCreateSeriesSchema):
|
||||
series_id: Optional[int] = Field(None)
|
||||
|
||||
|
|
@ -875,3 +880,99 @@ class UpdateCustomMetricsStatusSchema(BaseModel):
|
|||
|
||||
class SavedSearchSchema(FunnelSchema):
|
||||
filter: FlatSessionsSearchPayloadSchema = Field([])
|
||||
|
||||
|
||||
class CreateDashboardSchema(BaseModel):
|
||||
name: str = Field(..., min_length=1)
|
||||
is_public: bool = Field(default=False)
|
||||
is_pinned: bool = Field(default=False)
|
||||
metrics: Optional[List[int]] = Field(default=[])
|
||||
|
||||
class Config:
|
||||
alias_generator = attribute_to_camel_case
|
||||
|
||||
|
||||
class EditDashboardSchema(CreateDashboardSchema):
|
||||
is_public: Optional[bool] = Field(default=None)
|
||||
is_pinned: Optional[bool] = Field(default=None)
|
||||
|
||||
|
||||
class UpdateWidgetPayloadSchema(BaseModel):
|
||||
config: dict = Field(default={})
|
||||
|
||||
class Config:
|
||||
alias_generator = attribute_to_camel_case
|
||||
|
||||
|
||||
class AddWidgetToDashboardPayloadSchema(UpdateWidgetPayloadSchema):
|
||||
metric_id: int = Field(...)
|
||||
|
||||
class Config:
|
||||
alias_generator = attribute_to_camel_case
|
||||
|
||||
|
||||
# these values should match the keys in metrics table
|
||||
class TemplatePredefinedKeys(str, Enum):
|
||||
count_sessions = "count_sessions"
|
||||
avg_request_load_time = "avg_request_load_time"
|
||||
avg_page_load_time = "avg_page_load_time"
|
||||
avg_image_load_time = "avg_image_load_time"
|
||||
avg_dom_content_load_start = "avg_dom_content_load_start"
|
||||
avg_first_contentful_pixel = "avg_first_contentful_pixel"
|
||||
avg_visited_pages = "avg_visited_pages"
|
||||
avg_session_duration = "avg_session_duration"
|
||||
avg_pages_dom_buildtime = "avg_pages_dom_buildtime"
|
||||
avg_pages_response_time = "avg_pages_response_time"
|
||||
avg_response_time = "avg_response_time"
|
||||
avg_first_paint = "avg_first_paint"
|
||||
avg_dom_content_loaded = "avg_dom_content_loaded"
|
||||
avg_till_first_bit = "avg_till_first_byte"
|
||||
avg_time_to_interactive = "avg_time_to_interactive"
|
||||
count_requests = "count_requests"
|
||||
avg_time_to_render = "avg_time_to_render"
|
||||
avg_used_js_heap_size = "avg_used_js_heap_size"
|
||||
avg_cpu = "avg_cpu"
|
||||
avg_fps = "avg_fps"
|
||||
impacted_sessions_by_js_errors = "impacted_sessions_by_js_errors"
|
||||
domains_errors_4xx = "domains_errors_4xx"
|
||||
domains_errors_5xx = "domains_errors_5xx"
|
||||
errors_per_domains = "errors_per_domains"
|
||||
calls_errors = "calls_errors"
|
||||
errors_by_type = "errors_per_type"
|
||||
errors_by_origin = "resources_by_party"
|
||||
speed_index_by_location = "speed_location"
|
||||
slowest_domains = "slowest_domains"
|
||||
sessions_per_browser = "sessions_per_browser"
|
||||
time_to_render = "time_to_render"
|
||||
impacted_sessions_by_slow_pages = "impacted_sessions_by_slow_pages"
|
||||
memory_consumption = "memory_consumption"
|
||||
cpu_load = "cpu"
|
||||
frame_rate = "fps"
|
||||
crashes = "crashes"
|
||||
resources_vs_visually_complete = "resources_vs_visually_complete"
|
||||
pages_dom_buildtime = "pages_dom_buildtime"
|
||||
pages_response_time = "pages_response_time"
|
||||
pages_response_time_distribution = "pages_response_time_distribution"
|
||||
missing_resources = "missing_resources"
|
||||
slowest_resources = "slowest_resources"
|
||||
resources_fetch_time = "resources_loading_time"
|
||||
resource_type_vs_response_end = "resource_type_vs_response_end"
|
||||
resources_count_by_type = "resources_count_by_type"
|
||||
|
||||
|
||||
class TemplatePredefinedUnits(str, Enum):
|
||||
millisecond = "ms"
|
||||
minute = "min"
|
||||
memory = "mb"
|
||||
frame = "f/s"
|
||||
percentage = "%"
|
||||
count = "count"
|
||||
|
||||
|
||||
class CustomMetricAndTemplate(BaseModel):
|
||||
is_template: bool = Field(...)
|
||||
project_id: Optional[int] = Field(...)
|
||||
predefined_key: Optional[TemplatePredefinedKeys] = Field(...)
|
||||
|
||||
class Config:
|
||||
alias_generator = attribute_to_camel_case
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ pg_port=5432
|
|||
pg_user=postgres
|
||||
pg_timeout=30
|
||||
pg_minconn=45
|
||||
PG_RETRY_MAX=50
|
||||
PG_RETRY_INTERVAL=2
|
||||
put_S3_TTL=20
|
||||
sentryURL=
|
||||
sessions_bucket=mobs
|
||||
|
|
|
|||
10
ee/api/.gitignore
vendored
10
ee/api/.gitignore
vendored
|
|
@ -180,9 +180,6 @@ Pipfile
|
|||
/chalicelib/core/alerts.py
|
||||
/chalicelib/core/alerts_processor.py
|
||||
/chalicelib/core/announcements.py
|
||||
/chalicelib/blueprints/bp_app_api.py
|
||||
/chalicelib/blueprints/bp_core.py
|
||||
/chalicelib/blueprints/bp_core_crons.py
|
||||
/chalicelib/core/collaboration_slack.py
|
||||
/chalicelib/core/errors_favorite_viewed.py
|
||||
/chalicelib/core/events.py
|
||||
|
|
@ -237,7 +234,6 @@ Pipfile
|
|||
/chalicelib/utils/smtp.py
|
||||
/chalicelib/utils/strings.py
|
||||
/chalicelib/utils/TimeUTC.py
|
||||
/chalicelib/blueprints/app/__init__.py
|
||||
/routers/app/__init__.py
|
||||
/routers/crons/__init__.py
|
||||
/routers/subs/__init__.py
|
||||
|
|
@ -245,7 +241,6 @@ Pipfile
|
|||
/chalicelib/core/assist.py
|
||||
/auth/auth_apikey.py
|
||||
/auth/auth_jwt.py
|
||||
/chalicelib/blueprints/subs/bp_insights.py
|
||||
/build.sh
|
||||
/routers/core.py
|
||||
/routers/crons/core_crons.py
|
||||
|
|
@ -257,10 +252,11 @@ Pipfile
|
|||
/chalicelib/core/heatmaps.py
|
||||
/routers/subs/insights.py
|
||||
/schemas.py
|
||||
/chalicelib/blueprints/app/v1_api.py
|
||||
/routers/app/v1_api.py
|
||||
/chalicelib/core/custom_metrics.py
|
||||
/chalicelib/core/performance_event.py
|
||||
/chalicelib/core/saved_search.py
|
||||
/app_alerts.py
|
||||
/build_alerts.sh
|
||||
/routers/subs/metrics.py
|
||||
/routers/subs/v1_api.py
|
||||
/chalicelib/core/dashboards2.py
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ WORKDIR /work
|
|||
COPY . .
|
||||
RUN pip install -r requirements.txt
|
||||
RUN mv .env.default .env
|
||||
ENV APP_NAME chalice
|
||||
|
||||
# Add Tini
|
||||
# Startup daemon
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ COPY . .
|
|||
RUN pip install -r requirements.txt
|
||||
RUN mv .env.default .env && mv app_alerts.py app.py
|
||||
ENV pg_minconn 2
|
||||
ENV APP_NAME alerts
|
||||
|
||||
# Add Tini
|
||||
# Startup daemon
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
sudo yum update
|
||||
sudo yum install yum-utils
|
||||
sudo rpm --import https://repo.clickhouse.com/CLICKHOUSE-KEY.GPG
|
||||
sudo yum-config-manager --add-repo https://repo.clickhouse.com/rpm/stable/x86_64
|
||||
sudo yum update
|
||||
sudo service clickhouse-server restart
|
||||
|
||||
|
||||
#later mus use in clickhouse-client:
|
||||
#SET allow_experimental_window_functions = 1;
|
||||
|
|
@ -11,10 +11,10 @@ from starlette.responses import StreamingResponse, JSONResponse
|
|||
from chalicelib.utils import helper
|
||||
from chalicelib.utils import pg_client
|
||||
from routers import core, core_dynamic, ee, saml
|
||||
from routers.app import v1_api, v1_api_ee
|
||||
from routers.subs import v1_api
|
||||
from routers.crons import core_crons
|
||||
from routers.crons import core_dynamic_crons
|
||||
from routers.subs import dashboard
|
||||
from routers.subs import dashboard, insights, v1_api_ee
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ app.include_router(saml.public_app)
|
|||
app.include_router(saml.app)
|
||||
app.include_router(saml.app_apikey)
|
||||
app.include_router(dashboard.app)
|
||||
# app.include_router(insights.app)
|
||||
app.include_router(insights.app)
|
||||
app.include_router(v1_api.app_apikey)
|
||||
app.include_router(v1_api_ee.app_apikey)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,9 +1,9 @@
|
|||
from chalicelib.core import sessions_metas
|
||||
from chalicelib.utils import helper, dev
|
||||
from chalicelib.utils import ch_client
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
from chalicelib.core.dashboard import __get_constraint_values, __complete_missing_steps
|
||||
import schemas
|
||||
from chalicelib.core.dashboard import __get_basic_constraints, __get_meta_constraint
|
||||
from chalicelib.core.dashboard import __get_constraint_values, __complete_missing_steps
|
||||
from chalicelib.utils import ch_client
|
||||
from chalicelib.utils import helper, dev
|
||||
from chalicelib.utils.TimeUTC import TimeUTC
|
||||
|
||||
|
||||
def __transform_journey(rows):
|
||||
|
|
@ -42,7 +42,7 @@ def journey(project_id, startTimestamp=TimeUTC.now(delta_days=-1), endTimestamp=
|
|||
elif f["type"] == "EVENT_TYPE" and JOURNEY_TYPES.get(f["value"]):
|
||||
event_table = JOURNEY_TYPES[f["value"]]["table"]
|
||||
event_column = JOURNEY_TYPES[f["value"]]["column"]
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
meta_condition.append(f"sessions_metadata.project_id = %(project_id)s")
|
||||
meta_condition.append(f"sessions_metadata.datetime >= toDateTime(%(startTimestamp)s / 1000)")
|
||||
|
|
@ -303,7 +303,7 @@ def feature_retention(project_id, startTimestamp=TimeUTC.now(delta_days=-70), en
|
|||
elif f["type"] == "EVENT_VALUE":
|
||||
event_value = f["value"]
|
||||
default = False
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
meta_condition.append("sessions_metadata.user_id IS NOT NULL")
|
||||
meta_condition.append("not empty(sessions_metadata.user_id)")
|
||||
|
|
@ -404,7 +404,7 @@ def feature_acquisition(project_id, startTimestamp=TimeUTC.now(delta_days=-70),
|
|||
elif f["type"] == "EVENT_VALUE":
|
||||
event_value = f["value"]
|
||||
default = False
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
meta_condition.append("sessions_metadata.user_id IS NOT NULL")
|
||||
meta_condition.append("not empty(sessions_metadata.user_id)")
|
||||
|
|
@ -512,7 +512,7 @@ def feature_popularity_frequency(project_id, startTimestamp=TimeUTC.now(delta_da
|
|||
if f["type"] == "EVENT_TYPE" and JOURNEY_TYPES.get(f["value"]):
|
||||
event_table = JOURNEY_TYPES[f["value"]]["table"]
|
||||
event_column = JOURNEY_TYPES[f["value"]]["column"]
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
meta_condition.append("sessions_metadata.user_id IS NOT NULL")
|
||||
meta_condition.append("not empty(sessions_metadata.user_id)")
|
||||
|
|
@ -586,7 +586,7 @@ def feature_adoption(project_id, startTimestamp=TimeUTC.now(delta_days=-70), end
|
|||
elif f["type"] == "EVENT_VALUE":
|
||||
event_value = f["value"]
|
||||
default = False
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
meta_condition.append("sessions_metadata.user_id IS NOT NULL")
|
||||
meta_condition.append("not empty(sessions_metadata.user_id)")
|
||||
|
|
@ -672,7 +672,7 @@ def feature_adoption_top_users(project_id, startTimestamp=TimeUTC.now(delta_days
|
|||
elif f["type"] == "EVENT_VALUE":
|
||||
event_value = f["value"]
|
||||
default = False
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
meta_condition.append("user_id IS NOT NULL")
|
||||
meta_condition.append("not empty(sessions_metadata.user_id)")
|
||||
|
|
@ -742,7 +742,7 @@ def feature_adoption_daily_usage(project_id, startTimestamp=TimeUTC.now(delta_da
|
|||
elif f["type"] == "EVENT_VALUE":
|
||||
event_value = f["value"]
|
||||
default = False
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
meta_condition.append("sessions_metadata.project_id = %(project_id)s")
|
||||
meta_condition.append("sessions_metadata.datetime >= toDateTime(%(startTimestamp)s/1000)")
|
||||
|
|
@ -807,7 +807,7 @@ def feature_intensity(project_id, startTimestamp=TimeUTC.now(delta_days=-70), en
|
|||
if f["type"] == "EVENT_TYPE" and JOURNEY_TYPES.get(f["value"]):
|
||||
event_table = JOURNEY_TYPES[f["value"]]["table"]
|
||||
event_column = JOURNEY_TYPES[f["value"]]["column"]
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
meta_condition.append("sessions_metadata.project_id = %(project_id)s")
|
||||
meta_condition.append("sessions_metadata.datetime >= toDateTime(%(startTimestamp)s/1000)")
|
||||
|
|
@ -847,7 +847,7 @@ def users_active(project_id, startTimestamp=TimeUTC.now(delta_days=-70), endTime
|
|||
for f in filters:
|
||||
if f["type"] == "PERIOD" and f["value"] in ["DAY", "WEEK"]:
|
||||
period = f["value"]
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
extra_values["user_id"] = f["value"]
|
||||
period_function = PERIOD_TO_FUNCTION[period]
|
||||
|
|
@ -940,7 +940,7 @@ def users_slipping(project_id, startTimestamp=TimeUTC.now(delta_days=-70), endTi
|
|||
elif f["type"] == "EVENT_VALUE":
|
||||
event_value = f["value"]
|
||||
default = False
|
||||
elif f["type"] in [sessions_metas.meta_type.USERID, sessions_metas.meta_type.USERID_IOS]:
|
||||
elif f["type"] in [schemas.FilterType.user_id, schemas.FilterType.user_id_ios]:
|
||||
meta_condition.append(f"sessions_metadata.user_id = %(user_id)s")
|
||||
meta_condition.append("sessions_metadata.project_id = %(project_id)s")
|
||||
meta_condition.append("sessions_metadata.datetime >= toDateTime(%(startTimestamp)s/1000)")
|
||||
|
|
@ -1044,4 +1044,4 @@ def search(text, feature_type, project_id, platform=None):
|
|||
rows = ch.execute(ch_query, params)
|
||||
else:
|
||||
return []
|
||||
return [helper.dict_to_camel_case(row) for row in rows]
|
||||
return [helper.dict_to_camel_case(row) for row in rows]
|
||||
|
|
|
|||
|
|
@ -82,22 +82,22 @@ def get_projects(tenant_id, recording_state=False, gdpr=None, recorded=False, st
|
|||
rows = cur.fetchall()
|
||||
if recording_state:
|
||||
project_ids = [f'({r["project_id"]})' for r in rows]
|
||||
query = f"""SELECT projects.project_id, COALESCE(MAX(start_ts), 0) AS last
|
||||
FROM (VALUES {",".join(project_ids)}) AS projects(project_id)
|
||||
LEFT JOIN sessions USING (project_id)
|
||||
GROUP BY project_id;"""
|
||||
cur.execute(
|
||||
query=query
|
||||
)
|
||||
query = cur.mogrify(f"""SELECT projects.project_id, COALESCE(MAX(start_ts), 0) AS last
|
||||
FROM (VALUES {",".join(project_ids)}) AS projects(project_id)
|
||||
LEFT JOIN sessions USING (project_id)
|
||||
WHERE sessions.start_ts >= %(startDate)s AND sessions.start_ts <= %(endDate)s
|
||||
GROUP BY project_id;""",
|
||||
{"startDate": TimeUTC.now(delta_days=-3), "endDate": TimeUTC.now(delta_days=1)})
|
||||
|
||||
cur.execute(query=query)
|
||||
status = cur.fetchall()
|
||||
for r in rows:
|
||||
r["status"] = "red"
|
||||
for s in status:
|
||||
if s["project_id"] == r["project_id"]:
|
||||
if s["last"] < TimeUTC.now(-2):
|
||||
r["status"] = "red"
|
||||
elif s["last"] < TimeUTC.now(-1):
|
||||
if TimeUTC.now(-2) <= s["last"] < TimeUTC.now(-1):
|
||||
r["status"] = "yellow"
|
||||
else:
|
||||
elif s["last"] >= TimeUTC.now(-1):
|
||||
r["status"] = "green"
|
||||
break
|
||||
|
||||
|
|
|
|||
|
|
@ -25,5 +25,8 @@ class ClickHouseClient:
|
|||
def client(self):
|
||||
return self.__client
|
||||
|
||||
def format(self, query, params):
|
||||
return self.__client.substitute_params(query, params)
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ boto3==1.16.1
|
|||
pyjwt==1.7.1
|
||||
psycopg2-binary==2.8.6
|
||||
elasticsearch==7.9.1
|
||||
jira==2.0.0
|
||||
jira==3.1.1
|
||||
clickhouse-driver==0.2.2
|
||||
python3-saml==1.12.0
|
||||
|
||||
fastapi==0.74.1
|
||||
fastapi==0.75.0
|
||||
python-multipart==0.0.5
|
||||
uvicorn[standard]==0.17.5
|
||||
python-decouple==3.6
|
||||
|
|
|
|||
118
ee/scripts/helm/db/init_dbs/postgresql/1.5.5/1.5.5.sql
Normal file
118
ee/scripts/helm/db/init_dbs/postgresql/1.5.5/1.5.5.sql
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
BEGIN;
|
||||
CREATE OR REPLACE FUNCTION openreplay_version()
|
||||
RETURNS text AS
|
||||
$$
|
||||
SELECT 'v1.5.5-ee'
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dashboards
|
||||
(
|
||||
dashboard_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE,
|
||||
user_id integer NOT NULL REFERENCES users (user_id) ON DELETE SET NULL,
|
||||
name text NOT NULL,
|
||||
is_public boolean NOT NULL DEFAULT TRUE,
|
||||
is_pinned boolean NOT NULL DEFAULT FALSE,
|
||||
created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()),
|
||||
deleted_at timestamp NULL DEFAULT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE IF EXISTS metrics
|
||||
DROP CONSTRAINT IF EXISTS null_project_id_for_template_only,
|
||||
DROP CONSTRAINT IF EXISTS unique_key;
|
||||
|
||||
ALTER TABLE IF EXISTS metrics
|
||||
ADD COLUMN IF NOT EXISTS edited_at timestamp NULL DEFAULT NULL,
|
||||
ADD COLUMN IF NOT EXISTS is_pinned boolean NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS category text NULL DEFAULT 'custom',
|
||||
ADD COLUMN IF NOT EXISTS is_predefined boolean NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS is_template boolean NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS predefined_key text NULL DEFAULT NULL,
|
||||
ADD COLUMN IF NOT EXISTS default_config jsonb NOT NULL DEFAULT '{"col": 2,"row": 2,"position": 0}'::jsonb,
|
||||
ALTER COLUMN project_id DROP NOT NULL,
|
||||
ADD CONSTRAINT null_project_id_for_template_only
|
||||
CHECK ( (metrics.category != 'custom') != (metrics.project_id IS NOT NULL) ),
|
||||
ADD CONSTRAINT unique_key UNIQUE (predefined_key);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dashboard_widgets
|
||||
(
|
||||
widget_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
dashboard_id integer NOT NULL REFERENCES dashboards (dashboard_id) ON DELETE CASCADE,
|
||||
metric_id integer NOT NULL REFERENCES metrics (metric_id) ON DELETE CASCADE,
|
||||
user_id integer NOT NULL REFERENCES users (user_id) ON DELETE SET NULL,
|
||||
created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()),
|
||||
config jsonb NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
ALTER TYPE metric_view_type ADD VALUE IF NOT EXISTS 'areaChart';
|
||||
ALTER TYPE metric_view_type ADD VALUE IF NOT EXISTS 'barChart';
|
||||
ALTER TYPE metric_view_type ADD VALUE IF NOT EXISTS 'stackedBarChart';
|
||||
ALTER TYPE metric_view_type ADD VALUE IF NOT EXISTS 'stackedBarLineChart';
|
||||
ALTER TYPE metric_view_type ADD VALUE IF NOT EXISTS 'overview';
|
||||
ALTER TYPE metric_view_type ADD VALUE IF NOT EXISTS 'map';
|
||||
ALTER TYPE metric_type ADD VALUE IF NOT EXISTS 'predefined';
|
||||
|
||||
|
||||
INSERT INTO metrics (name, category, default_config, is_predefined, is_template, is_public, predefined_key, metric_type, view_type)
|
||||
VALUES ('Captured sessions', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'count_sessions', 'predefined', 'overview'),
|
||||
('Request Load Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_request_load_time', 'predefined', 'overview'),
|
||||
('Page Load Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_page_load_time', 'predefined', 'overview'),
|
||||
('Image Load Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_image_load_time', 'predefined', 'overview'),
|
||||
('DOM Content Load Start', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_dom_content_load_start', 'predefined', 'overview'),
|
||||
('First Meaningful paint', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_first_contentful_pixel', 'predefined', 'overview'),
|
||||
('No. of Visited Pages', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_visited_pages', 'predefined', 'overview'),
|
||||
('Session Duration', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_session_duration', 'predefined', 'overview'),
|
||||
('DOM Build Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_pages_dom_buildtime', 'predefined', 'overview'),
|
||||
('Pages Response Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_pages_response_time', 'predefined', 'overview'),
|
||||
('Response Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_response_time', 'predefined', 'overview'),
|
||||
('First Paint', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_first_paint', 'predefined', 'overview'),
|
||||
('DOM Content Loaded', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_dom_content_loaded', 'predefined', 'overview'),
|
||||
('Time Till First byte', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_till_first_byte', 'predefined', 'overview'),
|
||||
('Time To Interactive', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_time_to_interactive', 'predefined', 'overview'),
|
||||
('Captured requests', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'count_requests', 'predefined', 'overview'),
|
||||
('Time To Render', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_time_to_render', 'predefined', 'overview'),
|
||||
('Memory Consumption', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_used_js_heap_size', 'predefined', 'overview'),
|
||||
('CPU Load', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_cpu', 'predefined', 'overview'),
|
||||
('Frame rate', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_fps', 'predefined', 'overview'),
|
||||
|
||||
('Sessions Affected by JS Errors', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'impacted_sessions_by_js_errors', 'predefined', 'barChart'),
|
||||
('Top Domains with 4xx Fetch Errors', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'domains_errors_4xx', 'predefined', 'lineChart'),
|
||||
('Top Domains with 5xx Fetch Errors', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'domains_errors_5xx', 'predefined', 'lineChart'),
|
||||
('Errors per Domain', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'errors_per_domains', 'predefined', 'table'),
|
||||
('Fetch Calls with Errors', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'calls_errors', 'predefined', 'table'),
|
||||
('Errors by Type', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'errors_per_type', 'predefined', 'barChart'),
|
||||
('Errors by Origin', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'resources_by_party', 'predefined', 'stackedBarChart'),
|
||||
|
||||
('Speed Index by Location', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'speed_location', 'predefined', 'map'),
|
||||
('Slowest Domains', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'slowest_domains', 'predefined', 'table'),
|
||||
('Sessions per Browser', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'sessions_per_browser', 'predefined', 'table'),
|
||||
('Time To Render', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'time_to_render', 'predefined', 'areaChart'),
|
||||
('Sessions Impacted by Slow Pages', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'impacted_sessions_by_slow_pages', 'predefined', 'areaChart'),
|
||||
('Memory Consumption', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'memory_consumption', 'predefined', 'areaChart'),
|
||||
('CPU Load', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'cpu', 'predefined', 'areaChart'),
|
||||
('Frame Rate', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'fps', 'predefined', 'areaChart'),
|
||||
('Crashes', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'crashes', 'predefined', 'areaChart'),
|
||||
('Resources Loaded vs Visually Complete', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'resources_vs_visually_complete', 'predefined', 'areaChart'),
|
||||
('DOM Build Time', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'pages_dom_buildtime', 'predefined', 'areaChart'),
|
||||
('Pages Response Time', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'pages_response_time', 'predefined', 'areaChart'),
|
||||
('Pages Response Time Distribution', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'pages_response_time_distribution', 'predefined', 'barChart'),
|
||||
|
||||
('Missing Resources', 'resources', '{"col":4,"row":2,"position":0}', true, true, true, 'missing_resources', 'predefined', 'table'),
|
||||
('Slowest Resources', 'resources', '{"col":2,"row":2,"position":0}', true, true, true, 'slowest_resources', 'predefined', 'table'),
|
||||
('Resources Fetch Time', 'resources', '{"col":2,"row":2,"position":0}', true, true, true, 'resources_loading_time', 'predefined', 'table'),
|
||||
('Resource Loaded vs Response End', 'resources', '{"col":2,"row":2,"position":0}', true, true, true, 'resource_type_vs_response_end', 'predefined', 'stackedBarLineChart'),
|
||||
('Breakdown of Loaded Resources', 'resources', '{"col":2,"row":2,"position":0}', true, true, true, 'resources_count_by_type', 'predefined', 'stackedBarChart')
|
||||
ON CONFLICT (predefined_key) DO UPDATE
|
||||
SET name=excluded.name,
|
||||
category=excluded.category,
|
||||
default_config=excluded.default_config,
|
||||
is_predefined=excluded.is_predefined,
|
||||
is_template=excluded.is_template,
|
||||
is_public=excluded.is_public,
|
||||
metric_type=excluded.metric_type,
|
||||
view_type=excluded.view_type;
|
||||
|
|
@ -7,7 +7,7 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|||
CREATE OR REPLACE FUNCTION openreplay_version()
|
||||
RETURNS text AS
|
||||
$$
|
||||
SELECT 'v1.5.4-ee'
|
||||
SELECT 'v1.5.5-ee'
|
||||
$$ LANGUAGE sql IMMUTABLE;
|
||||
|
||||
|
||||
|
|
@ -106,6 +106,8 @@ $$
|
|||
('assigned_sessions'),
|
||||
('autocomplete'),
|
||||
('basic_authentication'),
|
||||
('dashboards'),
|
||||
('dashboard_widgets'),
|
||||
('errors'),
|
||||
('funnels'),
|
||||
('integrations'),
|
||||
|
|
@ -786,23 +788,33 @@ $$
|
|||
CREATE INDEX IF NOT EXISTS traces_user_id_idx ON traces (user_id);
|
||||
CREATE INDEX IF NOT EXISTS traces_tenant_id_idx ON traces (tenant_id);
|
||||
|
||||
CREATE TYPE metric_type AS ENUM ('timeseries','table');
|
||||
CREATE TYPE metric_view_type AS ENUM ('lineChart','progress','table','pieChart');
|
||||
CREATE TYPE metric_type AS ENUM ('timeseries','table', 'predefined');
|
||||
CREATE TYPE metric_view_type AS ENUM ('lineChart','progress','table','pieChart','areaChart','barChart','stackedBarChart','stackedBarLineChart','overview','map');
|
||||
CREATE TABLE IF NOT EXISTS metrics
|
||||
(
|
||||
metric_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE,
|
||||
user_id integer REFERENCES users (user_id) ON DELETE SET NULL,
|
||||
name text NOT NULL,
|
||||
is_public boolean NOT NULL DEFAULT FALSE,
|
||||
active boolean NOT NULL DEFAULT TRUE,
|
||||
created_at timestamp DEFAULT timezone('utc'::text, now()) not null,
|
||||
deleted_at timestamp,
|
||||
metric_type metric_type NOT NULL DEFAULT 'timeseries',
|
||||
view_type metric_view_type NOT NULL DEFAULT 'lineChart',
|
||||
metric_of text NOT NULL DEFAULT 'sessionCount',
|
||||
metric_value text[] NOT NULL DEFAULT '{}'::text[],
|
||||
metric_format text
|
||||
metric_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
project_id integer NULL REFERENCES projects (project_id) ON DELETE CASCADE,
|
||||
user_id integer REFERENCES users (user_id) ON DELETE SET NULL,
|
||||
name text NOT NULL,
|
||||
is_public boolean NOT NULL DEFAULT FALSE,
|
||||
active boolean NOT NULL DEFAULT TRUE,
|
||||
created_at timestamp default timezone('utc'::text, now()) not null,
|
||||
deleted_at timestamp,
|
||||
edited_at timestamp,
|
||||
metric_type metric_type NOT NULL DEFAULT 'timeseries',
|
||||
view_type metric_view_type NOT NULL DEFAULT 'lineChart',
|
||||
metric_of text NOT NULL DEFAULT 'sessionCount',
|
||||
metric_value text[] NOT NULL DEFAULT '{}'::text[],
|
||||
metric_format text,
|
||||
category text NULL DEFAULT 'custom',
|
||||
is_pinned boolean NOT NULL DEFAULT FALSE,
|
||||
is_predefined boolean NOT NULL DEFAULT FALSE,
|
||||
is_template boolean NOT NULL DEFAULT FALSE,
|
||||
predefined_key text NULL DEFAULT NULL,
|
||||
default_config jsonb NOT NULL DEFAULT '{"col": 2,"row": 2,"position": 0}'::jsonb,
|
||||
CONSTRAINT null_project_id_for_template_only
|
||||
CHECK ( (metrics.category != 'custom') != (metrics.project_id IS NOT NULL) ),
|
||||
CONSTRAINT unique_key UNIQUE (predefined_key)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS metrics_user_id_is_public_idx ON public.metrics (user_id, is_public);
|
||||
CREATE TABLE IF NOT EXISTS metric_series
|
||||
|
|
@ -817,6 +829,29 @@ $$
|
|||
);
|
||||
CREATE INDEX IF NOT EXISTS metric_series_metric_id_idx ON public.metric_series (metric_id);
|
||||
|
||||
|
||||
CREATE TABLE dashboards
|
||||
(
|
||||
dashboard_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
project_id integer NOT NULL REFERENCES projects (project_id) ON DELETE CASCADE,
|
||||
user_id integer NOT NULL REFERENCES users (user_id) ON DELETE SET NULL,
|
||||
name text NOT NULL,
|
||||
is_public boolean NOT NULL DEFAULT TRUE,
|
||||
is_pinned boolean NOT NULL DEFAULT FALSE,
|
||||
created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()),
|
||||
deleted_at timestamp NULL DEFAULT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE dashboard_widgets
|
||||
(
|
||||
widget_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
dashboard_id integer NOT NULL REFERENCES dashboards (dashboard_id) ON DELETE CASCADE,
|
||||
metric_id integer NOT NULL REFERENCES metrics (metric_id) ON DELETE CASCADE,
|
||||
user_id integer NOT NULL REFERENCES users (user_id) ON DELETE SET NULL,
|
||||
created_at timestamp NOT NULL DEFAULT timezone('utc'::text, now()),
|
||||
config jsonb NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS searches
|
||||
(
|
||||
search_id integer generated BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
|
|
@ -948,10 +983,13 @@ $$
|
|||
CREATE INDEX IF NOT EXISTS pages_session_id_timestamp_loadgt0NN_idx ON events.pages (session_id, timestamp) WHERE load_time > 0 AND load_time IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS pages_session_id_timestamp_visualgt0nn_idx ON events.pages (session_id, timestamp) WHERE visually_complete > 0 AND visually_complete IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS pages_timestamp_metgt0_idx ON events.pages (timestamp) WHERE response_time > 0 OR
|
||||
first_paint_time > 0 OR
|
||||
dom_content_loaded_time > 0 OR
|
||||
first_paint_time >
|
||||
0 OR
|
||||
dom_content_loaded_time >
|
||||
0 OR
|
||||
ttfb > 0 OR
|
||||
time_to_interactive > 0;
|
||||
time_to_interactive >
|
||||
0;
|
||||
CREATE INDEX IF NOT EXISTS pages_session_id_speed_indexgt0nn_idx ON events.pages (session_id, speed_index) WHERE speed_index > 0 AND speed_index IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS pages_session_id_timestamp_dom_building_timegt0nn_idx ON events.pages (session_id, timestamp, dom_building_time) WHERE dom_building_time > 0 AND dom_building_time IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS pages_base_path_session_id_timestamp_idx ON events.pages (base_path, session_id, timestamp);
|
||||
|
|
@ -1219,5 +1257,63 @@ $$
|
|||
$$
|
||||
LANGUAGE plpgsql;
|
||||
|
||||
INSERT INTO metrics (name, category, default_config, is_predefined, is_template, is_public, predefined_key, metric_type, view_type)
|
||||
VALUES ('Captured sessions', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'count_sessions', 'predefined', 'overview'),
|
||||
('Request Load Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_request_load_time', 'predefined', 'overview'),
|
||||
('Page Load Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_page_load_time', 'predefined', 'overview'),
|
||||
('Image Load Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_image_load_time', 'predefined', 'overview'),
|
||||
('DOM Content Load Start', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_dom_content_load_start', 'predefined', 'overview'),
|
||||
('First Meaningful paint', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_first_contentful_pixel', 'predefined', 'overview'),
|
||||
('No. of Visited Pages', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_visited_pages', 'predefined', 'overview'),
|
||||
('Session Duration', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_session_duration', 'predefined', 'overview'),
|
||||
('DOM Build Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_pages_dom_buildtime', 'predefined', 'overview'),
|
||||
('Pages Response Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_pages_response_time', 'predefined', 'overview'),
|
||||
('Response Time', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_response_time', 'predefined', 'overview'),
|
||||
('First Paint', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_first_paint', 'predefined', 'overview'),
|
||||
('DOM Content Loaded', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_dom_content_loaded', 'predefined', 'overview'),
|
||||
('Time Till First byte', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_till_first_byte', 'predefined', 'overview'),
|
||||
('Time To Interactive', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_time_to_interactive', 'predefined', 'overview'),
|
||||
('Captured requests', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'count_requests', 'predefined', 'overview'),
|
||||
('Time To Render', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_time_to_render', 'predefined', 'overview'),
|
||||
('Memory Consumption', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_used_js_heap_size', 'predefined', 'overview'),
|
||||
('CPU Load', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_cpu', 'predefined', 'overview'),
|
||||
('Frame rate', 'overview', '{"col":1,"row":1,"position":0}', true, true, true, 'avg_fps', 'predefined', 'overview'),
|
||||
|
||||
('Sessions Affected by JS Errors', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'impacted_sessions_by_js_errors', 'predefined', 'barChart'),
|
||||
('Top Domains with 4xx Fetch Errors', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'domains_errors_4xx', 'predefined', 'lineChart'),
|
||||
('Top Domains with 5xx Fetch Errors', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'domains_errors_5xx', 'predefined', 'lineChart'),
|
||||
('Errors per Domain', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'errors_per_domains', 'predefined', 'table'),
|
||||
('Fetch Calls with Errors', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'calls_errors', 'predefined', 'table'),
|
||||
('Errors by Type', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'errors_per_type', 'predefined', 'barChart'),
|
||||
('Errors by Origin', 'errors', '{"col":2,"row":2,"position":0}', true, true, true, 'resources_by_party', 'predefined', 'stackedBarChart'),
|
||||
|
||||
('Speed Index by Location', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'speed_location', 'predefined', 'map'),
|
||||
('Slowest Domains', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'slowest_domains', 'predefined', 'table'),
|
||||
('Sessions per Browser', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'sessions_per_browser', 'predefined', 'table'),
|
||||
('Time To Render', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'time_to_render', 'predefined', 'areaChart'),
|
||||
('Sessions Impacted by Slow Pages', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'impacted_sessions_by_slow_pages', 'predefined', 'areaChart'),
|
||||
('Memory Consumption', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'memory_consumption', 'predefined', 'areaChart'),
|
||||
('CPU Load', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'cpu', 'predefined', 'areaChart'),
|
||||
('Frame Rate', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'fps', 'predefined', 'areaChart'),
|
||||
('Crashes', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'crashes', 'predefined', 'areaChart'),
|
||||
('Resources Loaded vs Visually Complete', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'resources_vs_visually_complete', 'predefined', 'areaChart'),
|
||||
('DOM Build Time', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'pages_dom_buildtime', 'predefined', 'areaChart'),
|
||||
('Pages Response Time', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'pages_response_time', 'predefined', 'areaChart'),
|
||||
('Pages Response Time Distribution', 'performance', '{"col":2,"row":2,"position":0}', true, true, true, 'pages_response_time_distribution', 'predefined', 'barChart'),
|
||||
|
||||
('Missing Resources', 'resources', '{"col":2,"row":2,"position":0}', true, true, true, 'missing_resources', 'predefined', 'table'),
|
||||
('Slowest Resources', 'resources', '{"col":4,"row":2,"position":0}', true, true, true, 'slowest_resources', 'predefined', 'table'),
|
||||
('Resources Fetch Time', 'resources', '{"col":2,"row":2,"position":0}', true, true, true, 'resources_loading_time', 'predefined', 'table'),
|
||||
('Resource Loaded vs Response End', 'resources', '{"col":2,"row":2,"position":0}', true, true, true, 'resource_type_vs_response_end', 'predefined', 'stackedBarLineChart'),
|
||||
('Breakdown of Loaded Resources', 'resources', '{"col":2,"row":2,"position":0}', true, true, true, 'resources_count_by_type', 'predefined', 'stackedBarChart')
|
||||
ON CONFLICT (predefined_key) DO UPDATE
|
||||
SET name=excluded.name,
|
||||
category=excluded.category,
|
||||
default_config=excluded.default_config,
|
||||
is_predefined=excluded.is_predefined,
|
||||
is_template=excluded.is_template,
|
||||
is_public=excluded.is_public,
|
||||
metric_type=excluded.metric_type,
|
||||
view_type=excluded.view_type;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -14,6 +14,7 @@ const AGENT_DISCONNECT = "AGENT_DISCONNECTED";
|
|||
const AGENTS_CONNECTED = "AGENTS_CONNECTED";
|
||||
const NO_SESSIONS = "SESSION_DISCONNECTED";
|
||||
const SESSION_ALREADY_CONNECTED = "SESSION_ALREADY_CONNECTED";
|
||||
const SESSION_RECONNECTED = "SESSION_RECONNECTED";
|
||||
const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
const pubClient = createClient({url: REDIS_URL});
|
||||
const subClient = pubClient.duplicate();
|
||||
|
|
@ -309,6 +310,7 @@ module.exports = {
|
|||
debug && console.log(`notifying new session about agent-existence`);
|
||||
let agents_ids = await get_all_agents_ids(io, socket);
|
||||
io.to(socket.id).emit(AGENTS_CONNECTED, agents_ids);
|
||||
socket.to(socket.peerId).emit(SESSION_RECONNECTED, socket.id);
|
||||
}
|
||||
|
||||
} else if (c_sessions <= 0) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const AGENT_DISCONNECT = "AGENT_DISCONNECTED";
|
|||
const AGENTS_CONNECTED = "AGENTS_CONNECTED";
|
||||
const NO_SESSIONS = "SESSION_DISCONNECTED";
|
||||
const SESSION_ALREADY_CONNECTED = "SESSION_ALREADY_CONNECTED";
|
||||
const SESSION_RECONNECTED = "SESSION_RECONNECTED";
|
||||
|
||||
let io;
|
||||
const debug = process.env.debug === "1" || false;
|
||||
|
|
@ -287,6 +288,7 @@ module.exports = {
|
|||
debug && console.log(`notifying new session about agent-existence`);
|
||||
let agents_ids = await get_all_agents_ids(io, socket);
|
||||
io.to(socket.id).emit(AGENTS_CONNECTED, agents_ids);
|
||||
socket.to(socket.peerId).emit(SESSION_RECONNECTED, socket.id);
|
||||
}
|
||||
|
||||
} else if (c_sessions <= 0) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React, { lazy, Suspense } from 'react';
|
||||
import { Switch, Route, Redirect } from 'react-router';
|
||||
import { BrowserRouter, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
|
|
@ -5,26 +6,29 @@ import { Notification } from 'UI';
|
|||
import { Loader } from 'UI';
|
||||
import { fetchUserInfo } from 'Duck/user';
|
||||
import withSiteIdUpdater from 'HOCs/withSiteIdUpdater';
|
||||
import Login from 'Components/Login/Login';
|
||||
import ForgotPassword from 'Components/ForgotPassword/ForgotPassword';
|
||||
import UpdatePassword from 'Components/UpdatePassword/UpdatePassword';
|
||||
import ClientPure from 'Components/Client/Client';
|
||||
import OnboardingPure from 'Components/Onboarding/Onboarding';
|
||||
import SessionPure from 'Components/Session/Session';
|
||||
import LiveSessionPure from 'Components/Session/LiveSession';
|
||||
import AssistPure from 'Components/Assist';
|
||||
import BugFinderPure from 'Components/BugFinder/BugFinder';
|
||||
import DashboardPure from 'Components/Dashboard/Dashboard';
|
||||
import ErrorsPure from 'Components/Errors/Errors';
|
||||
const Login = lazy(() => import('Components/Login/Login'));
|
||||
const ForgotPassword = lazy(() => import('Components/ForgotPassword/ForgotPassword'));
|
||||
const UpdatePassword = lazy(() => import('Components/UpdatePassword/UpdatePassword'));
|
||||
const SessionPure = lazy(() => import('Components/Session/Session'));
|
||||
const LiveSessionPure = lazy(() => import('Components/Session/LiveSession'));
|
||||
const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding'));
|
||||
const ClientPure = lazy(() => import('Components/Client/Client'));
|
||||
const AssistPure = lazy(() => import('Components/Assist'));
|
||||
const BugFinderPure = lazy(() => import('Components/BugFinder/BugFinder'));
|
||||
const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard'));
|
||||
const ErrorsPure = lazy(() => import('Components/Errors/Errors'));
|
||||
const FunnelDetails = lazy(() => import('Components/Funnels/FunnelDetails'));
|
||||
const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDetails'));
|
||||
import WidgetViewPure from 'Components/Dashboard/components/WidgetView';
|
||||
import Header from 'Components/Header/Header';
|
||||
// import ResultsModal from 'Shared/Results/ResultsModal';
|
||||
import FunnelDetails from 'Components/Funnels/FunnelDetails';
|
||||
import FunnelIssueDetails from 'Components/Funnels/FunnelIssueDetails';
|
||||
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
|
||||
import { fetchList as fetchSiteList } from 'Duck/site';
|
||||
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
|
||||
import { fetchList as fetchAlerts } from 'Duck/alerts';
|
||||
import { fetchWatchdogStatus } from 'Duck/watchdogs';
|
||||
import { dashboardService } from "App/services";
|
||||
import { withStore } from 'App/mstore'
|
||||
|
||||
import APIClient from './api_client';
|
||||
import * as routes from './routes';
|
||||
|
|
@ -32,9 +36,12 @@ import { OB_DEFAULT_TAB } from 'App/routes';
|
|||
import Signup from './components/Signup/Signup';
|
||||
import { fetchTenants } from 'Duck/user';
|
||||
import { setSessionPath } from 'Duck/sessions';
|
||||
import { ModalProvider } from './components/Modal';
|
||||
import ModalRoot from './components/Modal/ModalRoot';
|
||||
|
||||
const BugFinder = withSiteIdUpdater(BugFinderPure);
|
||||
const Dashboard = withSiteIdUpdater(DashboardPure);
|
||||
const WidgetView = withSiteIdUpdater(WidgetViewPure);
|
||||
const Session = withSiteIdUpdater(SessionPure);
|
||||
const LiveSession = withSiteIdUpdater(LiveSessionPure);
|
||||
const Assist = withSiteIdUpdater(AssistPure);
|
||||
|
|
@ -46,7 +53,15 @@ const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails);
|
|||
const withSiteId = routes.withSiteId;
|
||||
const withObTab = routes.withObTab;
|
||||
|
||||
const METRICS_PATH = routes.metrics();
|
||||
const METRICS_DETAILS = routes.metricDetails();
|
||||
|
||||
const DASHBOARD_PATH = routes.dashboard();
|
||||
const DASHBOARD_SELECT_PATH = routes.dashboardSelected();
|
||||
const DASHBOARD_METRIC_CREATE_PATH = routes.dashboardMetricCreate();
|
||||
const DASHBOARD_METRIC_DETAILS_PATH = routes.dashboardMetricDetails();
|
||||
|
||||
// const WIDGET_PATAH = routes.dashboardMetric();
|
||||
const SESSIONS_PATH = routes.sessions();
|
||||
const ASSIST_PATH = routes.assist();
|
||||
const ERRORS_PATH = routes.errors();
|
||||
|
|
@ -62,6 +77,7 @@ const CLIENT_PATH = routes.client();
|
|||
const ONBOARDING_PATH = routes.onboarding();
|
||||
const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
|
||||
|
||||
@withStore
|
||||
@withRouter
|
||||
@connect((state) => {
|
||||
const siteId = state.getIn([ 'user', 'siteId' ]);
|
||||
|
|
@ -108,6 +124,8 @@ class Router extends React.Component {
|
|||
fetchInitialData = () => {
|
||||
Promise.all([
|
||||
this.props.fetchUserInfo().then(() => {
|
||||
const { mstore } = this.props
|
||||
mstore.initClient();
|
||||
this.props.fetchIntegrationVariables()
|
||||
}),
|
||||
this.props.fetchSiteList().then(() => {
|
||||
|
|
@ -153,54 +171,78 @@ class Router extends React.Component {
|
|||
{!hideHeader && <Header key="header"/>}
|
||||
<Notification />
|
||||
|
||||
<Switch key="content" >
|
||||
<Route path={ CLIENT_PATH } component={ Client } />
|
||||
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
|
||||
<Route
|
||||
path="/integrations/"
|
||||
render={
|
||||
({ location }) => {
|
||||
const client = new APIClient(jwt);
|
||||
switch (location.pathname) {
|
||||
case '/integrations/slack':
|
||||
client.post('integrations/slack/add', {
|
||||
code: location.search.split('=')[ 1 ],
|
||||
state: tenantId,
|
||||
});
|
||||
break;
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
<ModalProvider>
|
||||
<ModalRoot />
|
||||
<Switch key="content" >
|
||||
<Route path={ CLIENT_PATH } component={ Client } />
|
||||
<Route path={ withSiteId(ONBOARDING_PATH, siteIdList)} component={ Onboarding } />
|
||||
<Route
|
||||
path="/integrations/"
|
||||
render={
|
||||
({ location }) => {
|
||||
const client = new APIClient(jwt);
|
||||
switch (location.pathname) {
|
||||
case '/integrations/slack':
|
||||
client.post('integrations/slack/add', {
|
||||
code: location.search.split('=')[ 1 ],
|
||||
state: tenantId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
return <Redirect to={ CLIENT_PATH } />;
|
||||
}
|
||||
return <Redirect to={ CLIENT_PATH } />;
|
||||
}
|
||||
}
|
||||
/>
|
||||
{ onboarding &&
|
||||
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
|
||||
}
|
||||
{ siteIdList.length === 0 &&
|
||||
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
|
||||
}
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
|
||||
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(FUNNEL_PATH, siteIdList) } component={ Funnels } />
|
||||
<Route exact strict path={ withSiteId(FUNNEL_ISSUE_PATH, siteIdList) } component={ FunnelIssue } />
|
||||
<Route exact strict path={ withSiteId(SESSIONS_PATH, siteIdList) } component={ BugFinder } />
|
||||
<Route exact strict path={ withSiteId(SESSION_PATH, siteIdList) } component={ Session } />
|
||||
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } component={ LiveSession } />
|
||||
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } render={ (props) => <Session { ...props } live /> } />
|
||||
{ routes.redirects.map(([ fr, to ]) => (
|
||||
<Redirect key={ fr } exact strict from={ fr } to={ to } />
|
||||
)) }
|
||||
<Redirect to={ withSiteId(SESSIONS_PATH, siteId) } />
|
||||
/>
|
||||
{ onboarding &&
|
||||
<Redirect to={ withSiteId(ONBOARDING_REDIRECT_PATH, siteId)} />
|
||||
}
|
||||
{ siteIdList.length === 0 &&
|
||||
<Redirect to={ routes.client(routes.CLIENT_TABS.SITES) } />
|
||||
}
|
||||
|
||||
<Route exact strict path={ withSiteId(METRICS_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(METRICS_DETAILS, siteIdList) } component={ Dashboard } />
|
||||
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_SELECT_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_METRIC_CREATE_PATH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(DASHBOARD_METRIC_DETAILS_PATH, siteIdList) } component={ Dashboard } />
|
||||
|
||||
|
||||
|
||||
{/* <Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } />
|
||||
<Route exact strict path={ withSiteId(WIDGET_PATAH, siteIdList) } component={ Dashboard } /> */}
|
||||
|
||||
<Route exact strict path={ withSiteId(ASSIST_PATH, siteIdList) } component={ Assist } />
|
||||
<Route exact strict path={ withSiteId(ERRORS_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(ERROR_PATH, siteIdList) } component={ Errors } />
|
||||
<Route exact strict path={ withSiteId(FUNNEL_PATH, siteIdList) } component={ Funnels } />
|
||||
<Route exact strict path={ withSiteId(FUNNEL_ISSUE_PATH, siteIdList) } component={ FunnelIssue } />
|
||||
<Route exact strict path={ withSiteId(SESSIONS_PATH, siteIdList) } component={ BugFinder } />
|
||||
<Route exact strict path={ withSiteId(SESSION_PATH, siteIdList) } component={ Session } />
|
||||
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } component={ LiveSession } />
|
||||
<Route exact strict path={ withSiteId(LIVE_SESSION_PATH, siteIdList) } render={ (props) => <Session { ...props } live /> } />
|
||||
{ routes.redirects.map(([ fr, to ]) => (
|
||||
<Redirect key={ fr } exact strict from={ fr } to={ to } />
|
||||
)) }
|
||||
<Redirect to={ withSiteId(SESSIONS_PATH, siteId) } />
|
||||
</Switch>
|
||||
</ModalProvider>
|
||||
</Suspense>
|
||||
</Loader>
|
||||
:
|
||||
<Suspense fallback={<Loader loading={true} className="flex-1" />}>
|
||||
<Switch>
|
||||
<Route exact strict path={ FORGOT_PASSWORD } component={ ForgotPassword } />
|
||||
<Route exact strict path={ LOGIN_PATH } component={ changePassword ? UpdatePassword : Login } />
|
||||
{ !existingTenant && <Route exact strict path={ SIGNUP_PATH } component={ Signup } /> }
|
||||
<Redirect to={ LOGIN_PATH } />
|
||||
</Switch>
|
||||
</Loader> :
|
||||
<Switch>
|
||||
<Route exact strict path={ FORGOT_PASSWORD } component={ ForgotPassword } />
|
||||
<Route exact strict path={ LOGIN_PATH } component={ changePassword ? UpdatePassword : Login } />
|
||||
{ !existingTenant && <Route exact strict path={ SIGNUP_PATH } component={ Signup } /> }
|
||||
<Redirect to={ LOGIN_PATH } />
|
||||
</Switch>;
|
||||
</Suspense>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import store from 'App/store';
|
||||
|
||||
import { queried } from './routes';
|
||||
|
||||
const siteIdRequiredPaths = [
|
||||
|
|
@ -24,6 +23,8 @@ const siteIdRequiredPaths = [
|
|||
'/assist',
|
||||
'/heatmaps',
|
||||
'/custom_metrics',
|
||||
'/dashboards',
|
||||
'/metrics'
|
||||
// '/custom_metrics/sessions',
|
||||
];
|
||||
|
||||
|
|
@ -68,12 +69,16 @@ export default class APIClient {
|
|||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
fetch(path, params, options = { clean: true }) {
|
||||
fetch(path, params, options = { clean: true }) {
|
||||
if (params !== undefined) {
|
||||
const cleanedParams = options.clean ? clean(params) : params;
|
||||
this.init.body = JSON.stringify(cleanedParams);
|
||||
}
|
||||
|
||||
if (this.init.method === 'GET') {
|
||||
delete this.init.body;
|
||||
}
|
||||
|
||||
|
||||
let fetch = window.fetch;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="modal-root"></div>
|
||||
<div id="app"><p style="color: #eee;text-align: center;height: 100%;padding: 25%;">Loading...</p></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function AlertFormModal(props: Props) {
|
|||
const onDelete = async (instance) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`
|
||||
})) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const Alerts = props => {
|
|||
const onDelete = async (instance) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this alert?`
|
||||
})) {
|
||||
props.remove(instance.alertId).then(() => {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ class AutoComplete extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
const { ddOpen, query, loading, values } = this.state;
|
||||
const {
|
||||
const {
|
||||
optionMapping = defaultOptionMapping,
|
||||
valueToText = defaultValueToText,
|
||||
placeholder = 'Type to search...',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
|
||||
// import { DNDSource, DNDTarget } from 'Components/hocs/dnd';
|
||||
import Event, { TYPES } from 'Types/filter/event';
|
||||
import { operatorOptions } from 'Types/filter';
|
||||
import { editEvent, removeEvent, clearEvents, applyFilter } from 'Duck/filters';
|
||||
|
|
@ -25,8 +25,8 @@ const getLabel = ({ type }) => {
|
|||
return getPlaceholder({ type });
|
||||
};
|
||||
|
||||
@DNDTarget('event')
|
||||
@DNDSource('event')
|
||||
// @DNDTarget('event')
|
||||
// @DNDSource('event')
|
||||
@connect(state => ({
|
||||
isLastEvent: state.getIn([ 'filters', 'appliedFilter', 'events' ]).size === 1,
|
||||
}), { editEvent, removeEvent, clearEvents, applyFilter })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { Input } from 'semantic-ui-react';
|
||||
import { DNDContext } from 'Components/hocs/dnd';
|
||||
// import { DNDContext } from 'Components/hocs/dnd';
|
||||
import {
|
||||
addEvent, applyFilter, moveEvent, clearEvents, edit,
|
||||
addCustomFilter, addAttribute, setSearchQuery, setActiveFlow, setFilterOption
|
||||
|
|
@ -45,7 +45,7 @@ import SaveFilterButton from 'Shared/SaveFilterButton';
|
|||
setBlink,
|
||||
edit,
|
||||
})
|
||||
@DNDContext
|
||||
// @DNDContext
|
||||
export default class EventFilter extends React.PureComponent {
|
||||
state = { search: '', showFilterModal: false, showPlacehoder: true }
|
||||
fetchEventList = debounce(this.props.fetchEventList, 500)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class SlackAddForm extends React.PureComponent {
|
|||
remove = async (id) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, Delete',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this channel?`
|
||||
})) {
|
||||
this.props.remove(id);
|
||||
|
|
|
|||
49
frontend/app/components/Dashboard/NewDashboard.tsx
Normal file
49
frontend/app/components/Dashboard/NewDashboard.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { observer, useObserver } from "mobx-react-lite";
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import {
|
||||
dashboardSelected,
|
||||
withSiteId,
|
||||
} from 'App/routes';
|
||||
import DashboardSideMenu from './components/DashboardSideMenu';
|
||||
import { Loader } from 'UI';
|
||||
import DashboardRouter from './components/DashboardRouter';
|
||||
|
||||
function NewDashboard(props) {
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const loading = useObserver(() => dashboardStore.isLoading);
|
||||
|
||||
useEffect(() => {
|
||||
dashboardStore.fetchList().then((resp) => {
|
||||
if (parseInt(dashboardId) > 0) {
|
||||
dashboardStore.selectDashboardById(dashboardId);
|
||||
} else {
|
||||
dashboardStore.selectDefaultDashboard().then(({ dashboardId }) => {
|
||||
if (!history.location.pathname.includes('/metrics')) {
|
||||
history.push(withSiteId(dashboardSelected(dashboardId), siteId));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<div className="page-margin container-90">
|
||||
<div className="side-menu">
|
||||
<DashboardSideMenu siteId={siteId} />
|
||||
</div>
|
||||
<div className="side-menu-margined">
|
||||
<DashboardRouter siteId={siteId} />
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
export default withPageTitle('New Dashboard')(
|
||||
withRouter(observer(NewDashboard))
|
||||
);
|
||||
|
|
@ -22,7 +22,7 @@ function SideMenuSection({ title, items, onItemClick, setShowAlerts, siteId }) {
|
|||
)}
|
||||
|
||||
<div className={stl.divider} />
|
||||
<div className="my-3">
|
||||
<div className="my-3">
|
||||
<SideMenuitem
|
||||
id="menu-manage-alerts"
|
||||
title="Manage Alerts"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
onClick?: (event, index) => void;
|
||||
}
|
||||
function CustomMetriLineChart(props: Props) {
|
||||
const { data, params, seriesMap, colors, onClick = () => null } = props;
|
||||
const { data, params, seriesMap = [], colors, onClick = () => null } = props;
|
||||
return (
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<LineChart
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import React from 'react'
|
||||
import { Styles } from '../../common';
|
||||
import { AreaChart, ResponsiveContainer, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
|
||||
import { LineChart, Line, Legend } from 'recharts';
|
||||
import cn from 'classnames';
|
||||
import CountBadge from '../../common/CountBadge';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
// onClick?: (event, index) => void;
|
||||
}
|
||||
function CustomMetricOverviewChart(props: Props) {
|
||||
const { data } = props;
|
||||
console.log('data', data)
|
||||
const gradientDef = Styles.gradientDef();
|
||||
return (
|
||||
<div className="relative -mx-4">
|
||||
<div className="absolute flex items-start flex-col justify-center inset-0 p-3">
|
||||
<div className="mb-2 flex items-center" >
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<CountBadge
|
||||
// title={subtext}
|
||||
count={ countView(Math.round(data.value), data.unit) }
|
||||
change={ data.progress || 0 }
|
||||
unit={ data.unit }
|
||||
// className={textClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ResponsiveContainer height={ 100 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ {
|
||||
top: 85, right: 0, left: 0, bottom: 5,
|
||||
} }
|
||||
>
|
||||
{gradientDef}
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<XAxis hide {...Styles.xaxis} interval={4} dataKey="time" />
|
||||
<YAxis hide interval={ 0 } />
|
||||
<Area
|
||||
name={''}
|
||||
// unit={unit && ' ' + unit}
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomMetricOverviewChart
|
||||
|
||||
|
||||
const countView = (avg, unit) => {
|
||||
if (unit === 'mb') {
|
||||
if (!avg) return 0;
|
||||
const count = Math.trunc(avg / 1024 / 1024);
|
||||
return numberWithCommas(count);
|
||||
}
|
||||
if (unit === 'min') {
|
||||
if (!avg) return 0;
|
||||
const count = Math.trunc(avg);
|
||||
return numberWithCommas(count > 1000 ? count +'k' : count);
|
||||
}
|
||||
return avg ? numberWithCommas(avg): 0;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricOverviewChart';
|
||||
|
|
@ -35,8 +35,7 @@ function CustomMetricPieChart(props: Props) {
|
|||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<NoContent size="small" show={data.values && data.values.length === 0} >
|
||||
<NoContent size="small" show={!data.values || data.values.length === 0} style={{ minHeight: '240px'}}>
|
||||
<ResponsiveContainer height={ 220 } width="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
|
|
@ -52,105 +51,77 @@ function CustomMetricPieChart(props: Props) {
|
|||
activeIndex={1}
|
||||
onClick={onClickHandler}
|
||||
labelLine={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
|
||||
let radius2 = innerRadius + (outerRadius - innerRadius);
|
||||
let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
|
||||
let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
|
||||
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
|
||||
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius1 = 15 + innerRadius + (outerRadius - innerRadius);
|
||||
let radius2 = innerRadius + (outerRadius - innerRadius);
|
||||
let x2 = cx + radius1 * Math.cos(-midAngle * RADIAN);
|
||||
let y2 = cy + radius1 * Math.sin(-midAngle * RADIAN);
|
||||
let x1 = cx + radius2 * Math.cos(-midAngle * RADIAN);
|
||||
let y1 = cy + radius2 * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
const percentage = value * 100 / data.values.reduce((a, b) => a + b.sessionCount, 0);
|
||||
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
|
||||
return(
|
||||
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#3EAAAF" strokeWidth={1} />
|
||||
)
|
||||
}}
|
||||
label={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius = 20 + innerRadius + (outerRadius - innerRadius);
|
||||
let x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
let y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
const percentage = (value / data.values.reduce((a, b) => a + b.sessionCount, 0)) * 100;
|
||||
let name = data.values[index].name || 'Unidentified';
|
||||
name = name.length > 20 ? name.substring(0, 20) + '...' : name;
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fontWeight="400"
|
||||
fontSize="12px"
|
||||
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
|
||||
textAnchor={x > cx ? "start" : "end"}
|
||||
dominantBaseline="central"
|
||||
fill='#666'
|
||||
>
|
||||
{name || 'Unidentified'} {numberWithCommas(value)}
|
||||
</text>
|
||||
);
|
||||
}}
|
||||
// label={({
|
||||
// cx,
|
||||
// cy,
|
||||
// midAngle,
|
||||
// innerRadius,
|
||||
// outerRadius,
|
||||
// value,
|
||||
// index
|
||||
// }) => {
|
||||
// const RADIAN = Math.PI / 180;
|
||||
// const radius = 30 + innerRadius + (outerRadius - innerRadius);
|
||||
// const x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
// const y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
|
||||
// return (
|
||||
// <text
|
||||
// x={x}
|
||||
// y={y}
|
||||
// fill="#3EAAAF"
|
||||
// textAnchor={x > cx ? "start" : "end"}
|
||||
// dominantBaseline="top"
|
||||
// fontSize={10}
|
||||
// >
|
||||
// {data.values[index].name} ({value})
|
||||
// </text>
|
||||
// );
|
||||
// }}
|
||||
const percentage = value * 100 / data.values.reduce((a, b) => a + b.sessionCount, 0);
|
||||
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
|
||||
return(
|
||||
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#3EAAAF" strokeWidth={1} />
|
||||
)
|
||||
}}
|
||||
label={({
|
||||
cx,
|
||||
cy,
|
||||
midAngle,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
value,
|
||||
index
|
||||
}) => {
|
||||
const RADIAN = Math.PI / 180;
|
||||
let radius = 20 + innerRadius + (outerRadius - innerRadius);
|
||||
let x = cx + radius * Math.cos(-midAngle * RADIAN);
|
||||
let y = cy + radius * Math.sin(-midAngle * RADIAN);
|
||||
const percentage = (value / data.values.reduce((a, b) => a + b.sessionCount, 0)) * 100;
|
||||
let name = data.values[index].name || 'Unidentified';
|
||||
name = name.length > 20 ? name.substring(0, 20) + '...' : name;
|
||||
if (percentage<3){
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
fontWeight="400"
|
||||
fontSize="12px"
|
||||
// fontFamily="'Source Sans Pro', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'sans-serif'"
|
||||
textAnchor={x > cx ? "start" : "end"}
|
||||
dominantBaseline="central"
|
||||
fill='#666'
|
||||
>
|
||||
{name || 'Unidentified'} {numberWithCommas(value)}
|
||||
</text>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{data.values.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
|
||||
))}
|
||||
{data && data.values && data.values.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={Styles.colorsPie[index % Styles.colorsPie.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
</PieChart>
|
||||
|
||||
|
||||
</ResponsiveContainer>
|
||||
<div className="text-sm color-gray-medium">Top 5 </div>
|
||||
</NoContent>
|
||||
</div>
|
||||
</NoContent>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,29 +56,29 @@ function CustomMetricWidget(props: Props) {
|
|||
const isTable = metric.viewType === 'table';
|
||||
const isPieChart = metric.viewType === 'pieChart';
|
||||
|
||||
useEffect(() => {
|
||||
new APIClient()['post'](`/custom_metrics/${metricParams.metricId}/chart`, { ...metricParams, q: metric.name })
|
||||
.then(response => response.json())
|
||||
.then(({ errors, data }) => {
|
||||
if (errors) {
|
||||
console.log('err', errors)
|
||||
} else {
|
||||
const namesMap = data
|
||||
.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// new APIClient()['post'](`/custom_metrics/${metricParams.metricId}/chart`, { ...metricParams, q: metric.name })
|
||||
// .then(response => response.json())
|
||||
// .then(({ errors, data }) => {
|
||||
// if (errors) {
|
||||
// console.log('err', errors)
|
||||
// } else {
|
||||
// const namesMap = data
|
||||
// .map(i => Object.keys(i))
|
||||
// .flat()
|
||||
// .filter(i => i !== 'time' && i !== 'timestamp')
|
||||
// .reduce((unique: any, item: any) => {
|
||||
// if (!unique.includes(item)) {
|
||||
// unique.push(item);
|
||||
// }
|
||||
// return unique;
|
||||
// }, []);
|
||||
|
||||
setSeriesMap(namesMap);
|
||||
setData(getChartFormatter(period)(data));
|
||||
}
|
||||
}).finally(() => setLoading(false));
|
||||
}, [period])
|
||||
// setSeriesMap(namesMap);
|
||||
// setData(getChartFormatter(period)(data));
|
||||
// }
|
||||
// }).finally(() => setLoading(false));
|
||||
// }, [period])
|
||||
|
||||
const clickHandlerTable = (filters) => {
|
||||
const activeWidget = {
|
||||
|
|
|
|||
|
|
@ -61,27 +61,27 @@ function CustomMetricWidget(props: Props) {
|
|||
setLoading(true);
|
||||
|
||||
// fetch new data for the widget preview
|
||||
new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
|
||||
.then(response => response.json())
|
||||
.then(({ errors, data }) => {
|
||||
if (errors) {
|
||||
console.log('err', errors)
|
||||
} else {
|
||||
const namesMap = data
|
||||
.map(i => Object.keys(i))
|
||||
.flat()
|
||||
.filter(i => i !== 'time' && i !== 'timestamp')
|
||||
.reduce((unique: any, item: any) => {
|
||||
if (!unique.includes(item)) {
|
||||
unique.push(item);
|
||||
}
|
||||
return unique;
|
||||
}, []);
|
||||
// new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
|
||||
// .then(response => response.json())
|
||||
// .then(({ errors, data }) => {
|
||||
// if (errors) {
|
||||
// console.log('err', errors)
|
||||
// } else {
|
||||
// const namesMap = data
|
||||
// .map(i => Object.keys(i))
|
||||
// .flat()
|
||||
// .filter(i => i !== 'time' && i !== 'timestamp')
|
||||
// .reduce((unique: any, item: any) => {
|
||||
// if (!unique.includes(item)) {
|
||||
// unique.push(item);
|
||||
// }
|
||||
// return unique;
|
||||
// }, []);
|
||||
|
||||
setSeriesMap(namesMap);
|
||||
setData(getChartFormatter(period)(data));
|
||||
}
|
||||
}).finally(() => setLoading(false));
|
||||
// setSeriesMap(namesMap);
|
||||
// setData(getChartFormatter(period)(data));
|
||||
// }
|
||||
// }).finally(() => setLoading(false));
|
||||
}, [metric])
|
||||
|
||||
const onDateChange = (changedDates) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.bar {
|
||||
height: 10px;
|
||||
height: 5px;
|
||||
background-color: red;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const Bar = ({ className = '', width = 0, avg, domain, color }) => {
|
|||
<span className="font-medium">{`${avg}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm leading-3">{domain}</div>
|
||||
<div className="text-sm leading-3 color-gray-medium">{domain}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function BreakdownOfLoadedResources(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 28 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={params.density/7} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Resources" }}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name="CSS" dataKey="stylesheet" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar name="Images" dataKey="img" stackId="a" fill={Styles.colors[2]} />
|
||||
<Bar name="Scripts" dataKey="script" stackId="a" fill={Styles.colors[3]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default BreakdownOfLoadedResources;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './BreakdownOfLoadedResources'
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CPULoad(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CPULoad;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CPULoad'
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CallsErrors4xx(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{/* { data.namesMap.map((key, index) => (
|
||||
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
|
||||
))} */}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CallsErrors4xx;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CallsErrors4xx'
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CallsErrors5xx(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{/* { data.namesMap.map((key, index) => (
|
||||
<Line key={key} name={key} type="monotone" dataKey={key} stroke={Styles.colors[index]} fillOpacity={ 1 } strokeWidth={ 2 } strokeOpacity={ 0.8 } fill="url(#colorCount)" dot={false} />
|
||||
))} */}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CallsErrors5xx;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CallsErrors5xx'
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function Crashes(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Crashes"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Crashes;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Crashes'
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'pagesDomBuildtime';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function DomBuildingTime(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit="%"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(DomBuildingTime)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DomBuildingTime'
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ErrorsByOrigin(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name={<span className="float">1<sup>st</sup> Party</span>} dataKey="firstParty" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar name={<span className="float">3<sup>rd</sup> Party</span>} dataKey="thirdParty" stackId="a" fill={Styles.colors[2]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsByOrigin;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorsByOrigin'
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ErrorsByType(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name="Integrations" dataKey="integrations" stackId="a" fill={Styles.colors[0]}/>
|
||||
<Bar name="4xx" dataKey="4xx" stackId="a" fill={Styles.colors[1]} />
|
||||
<Bar name="5xx" dataKey="5xx" stackId="a" fill={Styles.colors[2]} />
|
||||
<Bar name="Javascript" dataKey="js" stackId="a" fill={Styles.colors[3]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsByType;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorsByType'
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import Bar from 'App/components/Dashboard/Widgets/ErrorsPerDomain/Bar';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ErrorsPerDomain(props: Props) {
|
||||
const { data } = props;
|
||||
console.log('ErrorsPerDomain', data);
|
||||
// const firstAvg = 10;
|
||||
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{data.chart.map((item, i) =>
|
||||
<Bar
|
||||
key={i}
|
||||
className="mb-2"
|
||||
avg={numberWithCommas(Math.round(item.errorsCount))}
|
||||
width={Math.round((item.errorsCount * 100) / firstAvg) - 10}
|
||||
domain={item.domain}
|
||||
color={Styles.colors[i]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorsPerDomain;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ErrorsPerDomain'
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function FPS(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
<AvgLabel text="Avg" className="ml-3" count={data.avgFps} />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
dataKey="avgFps"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default FPS;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FPS'
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function MemoryConsumption(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center justify-end mb-3">
|
||||
<AvgLabel text="Avg" unit="mb" className="ml-3" count={data.avgUsedJsHeapSize} />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "JS Heap Size (mb)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
unit=" mb"
|
||||
type="monotone"
|
||||
dataKey="avgFps"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MemoryConsumption;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MemoryConsumption'
|
||||
16
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MissingResources/Chart.js
vendored
Normal file
16
frontend/app/components/Dashboard/Widgets/PredefinedWidgets/MissingResources/Chart.js
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { AreaChart, Area } from 'recharts';
|
||||
import { Styles } from '../../common';
|
||||
|
||||
const Chart = ({ data, compare }) => {
|
||||
const colors = compare ? Styles.compareColors : Styles.colors;
|
||||
|
||||
return (
|
||||
<AreaChart width={ 90 } height={ 30 } data={ data.chart } >
|
||||
<Area type="monotone" dataKey="count" stroke={colors[0]} fill={colors[3]} fillOpacity={ 0.5 } />
|
||||
</AreaChart>
|
||||
);
|
||||
}
|
||||
|
||||
Chart.displayName = 'Chart';
|
||||
|
||||
export default Chart;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useState } from 'react'
|
||||
|
||||
const CopyPath = ({ data }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const copyHandler = () => {
|
||||
copy(data.url);
|
||||
setCopied(true);
|
||||
setTimeout(function() {
|
||||
setCopied(false)
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cursor-pointer color-teal" onClick={copyHandler}>
|
||||
{ copied ? 'Copied' : 'Copy Path'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyPath
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, Table } from '../../common';
|
||||
import { List } from 'immutable';
|
||||
|
||||
import Chart from './Chart';
|
||||
import ResourceInfo from './ResourceInfo';
|
||||
import CopyPath from './CopyPath';
|
||||
|
||||
const cols = [
|
||||
{
|
||||
key: 'resource',
|
||||
title: 'Resource',
|
||||
Component: ResourceInfo,
|
||||
width: '40%',
|
||||
},
|
||||
{
|
||||
key: 'sessions',
|
||||
title: 'Sessions',
|
||||
toText: count => `${ count > 1000 ? Math.trunc(count / 1000) : count }${ count > 1000 ? 'k' : '' }`,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
key: 'trend',
|
||||
title: 'Trend',
|
||||
Component: Chart,
|
||||
width: '20%',
|
||||
},
|
||||
{
|
||||
key: 'copy-path',
|
||||
title: '',
|
||||
Component: CopyPath,
|
||||
cellClass: 'invisible group-hover:visible text-right',
|
||||
width: '20%',
|
||||
}
|
||||
];
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function MissingResources(props: Props) {
|
||||
const { data } = props;
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
title="No resources missing."
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<div style={{ height: '240px'}}>
|
||||
<Table
|
||||
small
|
||||
cols={ cols }
|
||||
rows={ List(data.chart) }
|
||||
rowClass="group"
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MissingResources;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { diffFromNowString } from 'App/date';
|
||||
import { TextEllipsis } from 'UI';
|
||||
|
||||
import styles from './resourceInfo.css';
|
||||
|
||||
export default class ResourceInfo extends React.PureComponent {
|
||||
render() {
|
||||
const { data } = this.props;
|
||||
return (
|
||||
<div className="flex flex-col" >
|
||||
<TextEllipsis className={ styles.name } text={ data.name } hintText={ data.url } />
|
||||
<div className={ styles.timings }>
|
||||
{ data.endedAt && data.startedAt && `${ diffFromNowString(data.endedAt) } ago - ${ diffFromNowString(data.startedAt) } old` }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MissingResources'
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.name {
|
||||
letter-spacing: -.04em;
|
||||
font-size: .9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.timings {
|
||||
color: $gray-medium;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
ComposedChart, Bar, CartesianGrid, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis, Tooltip
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ResourceLoadedVsResponseEnd(props: Props) {
|
||||
const { data } = props;
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<ComposedChart
|
||||
data={data.chart}
|
||||
margin={ Styles.chartMargins}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={3}
|
||||
interval={(params.density / 7)}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Resources" }}
|
||||
yAxisId="left"
|
||||
tickFormatter={val => Styles.tickFormatter(val, 'ms')}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: "Response End (ms)",
|
||||
position: "insideRight",
|
||||
offset: 0
|
||||
}}
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tickFormatter={val => Styles.tickFormatter(val, 'ms')}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Legend />
|
||||
<Bar minPointSize={1} yAxisId="left" name="XHR" dataKey="xhr" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar yAxisId="left" name="Other" dataKey="total" stackId="a" fill={Styles.colors[2]} />
|
||||
<Line
|
||||
yAxisId="right"
|
||||
strokeWidth={2}
|
||||
name="Response End"
|
||||
type="monotone"
|
||||
dataKey="avgResponseEnd"
|
||||
stroke={Styles.lineColor}
|
||||
dot={false}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResourceLoadedVsResponseEnd;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResourceLoadedVsResponseEnd'
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
ComposedChart, Bar, CartesianGrid, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis, Tooltip
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function ResourceLoadedVsVisuallyComplete(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<ComposedChart
|
||||
data={data.chart}
|
||||
margin={ Styles.chartMargins}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={3}
|
||||
interval={(params.density / 7)}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Visually Complete (ms)" }}
|
||||
yAxisId="left"
|
||||
tickFormatter={val => Styles.tickFormatter(val, 'ms')}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: "Number of Resources",
|
||||
position: "insideRight",
|
||||
offset: 0
|
||||
}}
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Legend />
|
||||
<Bar minPointSize={1} yAxisId="right" name="Images" type="monotone" dataKey="types.img" stackId="a" fill={Styles.colors[0]} />
|
||||
<Bar yAxisId="right" name="Scripts" type="monotone" dataKey="types.script" stackId="a" fill={Styles.colors[2]} />
|
||||
<Bar yAxisId="right" name="CSS" type="monotone" dataKey="types.stylesheet" stackId="a" fill={Styles.colors[4]} />
|
||||
<Line
|
||||
yAxisId="left"
|
||||
name="Visually Complete"
|
||||
type="monotone"
|
||||
dataKey="avgTimeToRender"
|
||||
stroke={Styles.lineColor }
|
||||
dot={false}
|
||||
unit=" ms"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResourceLoadedVsVisuallyComplete;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResourceLoadedVsVisuallyComplete'
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import React from 'react';
|
||||
import { NoContent, DropdownPlain } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'resourcesLoadingTime';
|
||||
export const RESOURCE_OPTIONS = [
|
||||
{ text: 'All', value: 'all', },
|
||||
{ text: 'JS', value: "SCRIPT", },
|
||||
{ text: 'CSS', value: "STYLESHEET", },
|
||||
{ text: 'Fetch', value: "REQUEST", },
|
||||
{ text: 'Image', value: "IMG", },
|
||||
{ text: 'Media', value: "MEDIA", },
|
||||
{ text: 'Other', value: "OTHER", },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function ResourceLoadingTime(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
const [autoCompleteSelected, setSutoCompleteSelected] = React.useState('');
|
||||
const [type, setType] = React.useState('');
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
setSutoCompleteSelected(params.value);
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
const writeOption = (e, { name, value }) => {
|
||||
// this.setState({ [name]: value })
|
||||
setType(value);
|
||||
const _params = { density: 70 } // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, this.props.period, this.props.platform, { ..._params, [ name ]: value === 'all' ? null : value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<DropdownPlain
|
||||
disabled={!!autoCompleteSelected}
|
||||
name="type"
|
||||
label="Resource"
|
||||
options={ RESOURCE_OPTIONS }
|
||||
onChange={ writeOption }
|
||||
defaultValue={'all'}
|
||||
wrapperStyle={{
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
left: '170px',
|
||||
}}
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 200 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
unit=" ms"
|
||||
type="monotone"
|
||||
dataKey="avg"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(ResourceLoadingTime)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResourceLoadingTime'
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles, AvgLabel } from '../../common';
|
||||
import { withRequest } from 'HOCs'
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
import WidgetAutoComplete from 'Shared/WidgetAutoComplete';
|
||||
import { toUnderscore } from 'App/utils';
|
||||
|
||||
const WIDGET_KEY = 'pagesResponseTime';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
optionsLoading: any
|
||||
fetchOptions: any
|
||||
options: any
|
||||
}
|
||||
function ResponseTime(props: Props) {
|
||||
const { data, optionsLoading } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
|
||||
const onSelect = (params) => {
|
||||
const _params = { density: 70 }
|
||||
console.log('params', params) // TODO reload the data with new params;
|
||||
// this.props.fetchWidget(WIDGET_KEY, dashbaordStore.period, props.platform, { ..._params, url: params.value })
|
||||
}
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<WidgetAutoComplete
|
||||
loading={optionsLoading}
|
||||
fetchOptions={props.fetchOptions}
|
||||
options={props.options}
|
||||
onSelect={onSelect}
|
||||
placeholder="Search for Page"
|
||||
/>
|
||||
<AvgLabel className="ml-auto" text="Avg" count={Math.round(data.avg)} unit="ms" />
|
||||
</div>
|
||||
<ResponsiveContainer height={ 207 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Page Response Time (ms)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Avg"
|
||||
type="monotone"
|
||||
unit=" ms"
|
||||
dataKey="avgCpu"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRequest({
|
||||
dataName: "options",
|
||||
initialData: [],
|
||||
dataWrapper: data => data,
|
||||
loadingName: 'optionsLoading',
|
||||
requestName: "fetchOptions",
|
||||
endpoint: '/dashboard/' + toUnderscore(WIDGET_KEY) + '/search',
|
||||
method: 'GET'
|
||||
})(ResponseTime)
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './ResponseTime'
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function SessionsAffectedByJSErrors(props: Props) {
|
||||
const { data } = props;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<BarChart
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
syncId="errorsPerType"
|
||||
// syncId={ showSync ? "errorsPerType" : undefined }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
// interval={params.density/7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
label={{ ...Styles.axisLabelLeft, value: "Number of Errors" }}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Bar minPointSize={1} name="Sessions" dataKey="sessionsCount" stackId="a" fill={Styles.colors[0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsAffectedByJSErrors;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionsAffectedByJSErrors'
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { NoContent } from 'UI';
|
||||
import { Styles } from '../../common';
|
||||
import {
|
||||
AreaChart, Area,
|
||||
BarChart, Bar, CartesianGrid, Tooltip,
|
||||
LineChart, Line, Legend, ResponsiveContainer,
|
||||
XAxis, YAxis
|
||||
} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function SessionsImpactedBySlowRequests(props: Props) {
|
||||
const { data } = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
const params = { density: 70 }
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ data.chart.length === 0 }
|
||||
>
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ Styles.chartMargins }
|
||||
>
|
||||
{gradientDef}
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<XAxis {...Styles.xaxis} dataKey="time" interval={(params.density/7)} />
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{ ...Styles.axisLabelLeft, value: "CPU Load (%)" }}
|
||||
/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<Area
|
||||
name="Sessions"
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke={Styles.colors[0]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsImpactedBySlowRequests;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionsImpactedBySlowRequests'
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue