Merge branch 'main' of github.com:openreplay/openreplay into pagination
This commit is contained in:
commit
dd62a7116f
27 changed files with 192 additions and 139 deletions
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.9.7-slim
|
||||
FROM python:3.9.10-slim
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
WORKDIR /work
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.9.7-slim
|
||||
FROM python:3.9.10-slim
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
WORKDIR /work
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.9.7-slim
|
||||
FROM python:3.9.10-slim
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
WORKDIR /work
|
||||
COPY . .
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ def __try_live(project_id, data: schemas.CreateCustomMetricsSchema):
|
|||
if data.view_type == schemas.MetricTimeseriesViewType.progress:
|
||||
r = {"count": results[-1]}
|
||||
diff = s.filter.endDate - s.filter.startDate
|
||||
s.filter.startDate = data.endDate
|
||||
s.filter.endDate = data.endDate - diff
|
||||
s.filter.endDate = s.filter.startDate
|
||||
s.filter.startDate = s.filter.endDate - diff
|
||||
r["previousCount"] = 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)
|
||||
|
|
|
|||
|
|
@ -754,22 +754,20 @@ def format_first_stack_frame(error):
|
|||
def stats(project_id, user_id, startTimestamp=TimeUTC.now(delta_days=-7), endTimestamp=TimeUTC.now()):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
"""
|
||||
SELECT COUNT(errors.*) AS unresolved_and_unviewed
|
||||
FROM public.errors
|
||||
INNER JOIN (SELECT root_error.error_id
|
||||
FROM events.errors
|
||||
INNER JOIN public.errors AS root_error USING (error_id)
|
||||
WHERE project_id = %(project_id)s
|
||||
AND timestamp >= %(startTimestamp)s
|
||||
AND timestamp <= %(endTimestamp)s
|
||||
AND source = 'js_exception') AS timed_errors USING (error_id)
|
||||
LEFT JOIN (SELECT error_id FROM public.user_viewed_errors WHERE user_id = %(user_id)s) AS user_viewed
|
||||
USING (error_id)
|
||||
WHERE user_viewed.error_id ISNULL
|
||||
AND errors.project_id = %(project_id)s
|
||||
AND errors.status = 'unresolved'
|
||||
AND errors.source = 'js_exception';""",
|
||||
"""WITH user_viewed AS (SELECT error_id FROM public.user_viewed_errors WHERE user_id = %(user_id)s)
|
||||
SELECT COUNT(timed_errors.*) AS unresolved_and_unviewed
|
||||
FROM (SELECT root_error.error_id
|
||||
FROM events.errors
|
||||
INNER JOIN public.errors AS root_error USING (error_id)
|
||||
LEFT JOIN user_viewed USING (error_id)
|
||||
WHERE project_id = %(project_id)s
|
||||
AND timestamp >= %(startTimestamp)s
|
||||
AND timestamp <= %(endTimestamp)s
|
||||
AND source = 'js_exception'
|
||||
AND root_error.status = 'unresolved'
|
||||
AND user_viewed.error_id ISNULL
|
||||
LIMIT 1
|
||||
) AS timed_errors;""",
|
||||
{"project_id": project_id, "user_id": user_id, "startTimestamp": startTimestamp,
|
||||
"endTimestamp": endTimestamp})
|
||||
cur.execute(query=query)
|
||||
|
|
@ -777,4 +775,4 @@ def stats(project_id, user_id, startTimestamp=TimeUTC.now(delta_days=-7), endTim
|
|||
|
||||
return {
|
||||
"data": helper.dict_to_camel_case(row)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ ALLOW_UPDATE_FOR = ["name", "filter"]
|
|||
# events.event_type.VIEW_IOS.ui_type, events.event_type.CUSTOM_IOS.ui_type, ]
|
||||
# return [s for s in stages if s["type"] in ALLOW_TYPES and s.get("value") is not None]
|
||||
|
||||
def __transform_old_funnels(events):
|
||||
for e in events:
|
||||
if not isinstance(e.get("value"), list):
|
||||
e["value"] = [e["value"]]
|
||||
return events
|
||||
|
||||
|
||||
def create(project_id, user_id, name, filter: schemas.FunnelSearchPayloadSchema, is_public):
|
||||
helper.delete_keys_from_dict(filter, REMOVE_KEYS)
|
||||
|
|
@ -97,6 +103,9 @@ def get_by_user(project_id, user_id, range_value=None, start_date=None, end_date
|
|||
row["createdAt"] = TimeUTC.datetime_to_timestamp(row["createdAt"])
|
||||
if details:
|
||||
# row["filter"]["events"] = filter_stages(row["filter"]["events"])
|
||||
if row.get("filter") is not None and row["filter"].get("events") is not None:
|
||||
row["filter"]["events"] = __transform_old_funnels(row["filter"]["events"])
|
||||
|
||||
get_start_end_time(filter_d=row["filter"], range_value=range_value, start_date=start_date,
|
||||
end_date=end_date)
|
||||
counts = sessions.search2_pg(data=schemas.SessionsSearchPayloadSchema.parse_obj(row["filter"]),
|
||||
|
|
@ -248,7 +257,8 @@ def get(funnel_id, project_id, user_id, flatten=True):
|
|||
f = helper.dict_to_camel_case(cur.fetchone())
|
||||
if f is None:
|
||||
return None
|
||||
|
||||
if f.get("filter") is not None and f["filter"].get("events") is not None:
|
||||
f["filter"]["events"] = __transform_old_funnels(f["filter"]["events"])
|
||||
f["createdAt"] = TimeUTC.datetime_to_timestamp(f["createdAt"])
|
||||
# f["filter"]["events"] = filter_stages(stages=f["filter"]["events"])
|
||||
if flatten:
|
||||
|
|
|
|||
|
|
@ -827,6 +827,7 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
is_any = _isAny_opreator(f.operator)
|
||||
if is_any or len(f.value) == 0:
|
||||
continue
|
||||
f.value = helper.values_for_operator(value=f.value, op=f.operator)
|
||||
op = __get_sql_operator(f.operator)
|
||||
e_k_f = e_k + f"_fetch{j}"
|
||||
full_args = {**full_args, **_multiple_values(f.value, value_key=e_k_f)}
|
||||
|
|
@ -837,7 +838,8 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
apply = True
|
||||
elif f.type == schemas.FetchFilterType._status_code:
|
||||
event_where.append(
|
||||
_multiple_conditions(f"main.status_code {op} %({e_k_f})s", f.value, value_key=e_k_f))
|
||||
_multiple_conditions(f"main.status_code {f.operator} %({e_k_f})s", f.value,
|
||||
value_key=e_k_f))
|
||||
apply = True
|
||||
elif f.type == schemas.FetchFilterType._method:
|
||||
event_where.append(
|
||||
|
|
@ -845,7 +847,7 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
apply = True
|
||||
elif f.type == schemas.FetchFilterType._duration:
|
||||
event_where.append(
|
||||
_multiple_conditions(f"main.duration {op} %({e_k_f})s", f.value, value_key=e_k_f))
|
||||
_multiple_conditions(f"main.duration {f.operator} %({e_k_f})s", f.value, value_key=e_k_f))
|
||||
apply = True
|
||||
elif f.type == schemas.FetchFilterType._request_body:
|
||||
event_where.append(
|
||||
|
|
@ -865,6 +867,7 @@ def search_query_parts(data, error_status, errors_only, favorite_only, issue, pr
|
|||
is_any = _isAny_opreator(f.operator)
|
||||
if is_any or len(f.value) == 0:
|
||||
continue
|
||||
f.value = helper.values_for_operator(value=f.value, op=f.operator)
|
||||
op = __get_sql_operator(f.operator)
|
||||
e_k_f = e_k + f"_graphql{j}"
|
||||
full_args = {**full_args, **_multiple_values(f.value, value_key=e_k_f)}
|
||||
|
|
|
|||
|
|
@ -545,7 +545,7 @@ def get_issues(stages, rows, first_stage=None, last_stage=None, drop_only=False)
|
|||
@dev.timed
|
||||
def get_top_insights(filter_d, project_id):
|
||||
output = []
|
||||
stages = filter_d["events"]
|
||||
stages = filter_d.get("events", [])
|
||||
# TODO: handle 1 stage alone
|
||||
if len(stages) == 0:
|
||||
print("no stages found")
|
||||
|
|
|
|||
|
|
@ -103,13 +103,16 @@ def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schem
|
|||
@app.get('/{projectId}/events/search', tags=["events"])
|
||||
def events_search(projectId: int, q: str,
|
||||
type: Union[schemas.FilterType, schemas.EventType,
|
||||
schemas.PerformanceEventType, schemas.FetchFilterType] = None,
|
||||
schemas.PerformanceEventType, schemas.FetchFilterType,
|
||||
schemas.GraphqlFilterType] = None,
|
||||
key: str = None,
|
||||
source: str = None, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
if len(q) == 0:
|
||||
return {"data": []}
|
||||
if type in [schemas.FetchFilterType._url]:
|
||||
type = schemas.EventType.request
|
||||
elif type in [schemas.GraphqlFilterType._name]:
|
||||
type = schemas.EventType.graphql
|
||||
elif isinstance(type, schemas.PerformanceEventType):
|
||||
if type in [schemas.PerformanceEventType.location_dom_complete,
|
||||
schemas.PerformanceEventType.location_largest_contentful_paint_time,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.9.7-slim
|
||||
FROM python:3.9.10-slim
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apt-get update && apt-get install -y pkg-config libxmlsec1-dev gcc && rm -rf /var/lib/apt/lists/*
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.9.7-slim
|
||||
FROM python:3.9.10-slim
|
||||
LABEL Maintainer="Rajesh Rajendran<rjshrjndrn@gmail.com>"
|
||||
LABEL Maintainer="KRAIEM Taha Yassine<tahayk2@gmail.com>"
|
||||
RUN apt-get update && apt-get install -y pkg-config libxmlsec1-dev gcc && rm -rf /var/lib/apt/lists/*
|
||||
|
|
|
|||
|
|
@ -766,22 +766,20 @@ def format_first_stack_frame(error):
|
|||
def stats(project_id, user_id, startTimestamp=TimeUTC.now(delta_days=-7), endTimestamp=TimeUTC.now()):
|
||||
with pg_client.PostgresClient() as cur:
|
||||
query = cur.mogrify(
|
||||
"""
|
||||
SELECT COUNT(errors.*) AS unresolved_and_unviewed
|
||||
FROM public.errors
|
||||
INNER JOIN (SELECT root_error.error_id
|
||||
FROM events.errors
|
||||
INNER JOIN public.errors AS root_error USING (error_id)
|
||||
WHERE project_id = %(project_id)s
|
||||
AND timestamp >= %(startTimestamp)s
|
||||
AND timestamp <= %(endTimestamp)s
|
||||
AND source = 'js_exception') AS timed_errors USING (error_id)
|
||||
LEFT JOIN (SELECT error_id FROM public.user_viewed_errors WHERE user_id = %(user_id)s) AS user_viewed
|
||||
USING (error_id)
|
||||
WHERE user_viewed.error_id ISNULL
|
||||
AND errors.project_id = %(project_id)s
|
||||
AND errors.status = 'unresolved'
|
||||
AND errors.source = 'js_exception';""",
|
||||
"""WITH user_viewed AS (SELECT error_id FROM public.user_viewed_errors WHERE user_id = %(user_id)s)
|
||||
SELECT COUNT(timed_errors.*) AS unresolved_and_unviewed
|
||||
FROM (SELECT root_error.error_id
|
||||
FROM events.errors
|
||||
INNER JOIN public.errors AS root_error USING (error_id)
|
||||
LEFT JOIN user_viewed USING (error_id)
|
||||
WHERE project_id = %(project_id)s
|
||||
AND timestamp >= %(startTimestamp)s
|
||||
AND timestamp <= %(endTimestamp)s
|
||||
AND source = 'js_exception'
|
||||
AND root_error.status = 'unresolved'
|
||||
AND user_viewed.error_id ISNULL
|
||||
LIMIT 1
|
||||
) AS timed_errors;""",
|
||||
{"project_id": project_id, "user_id": user_id, "startTimestamp": startTimestamp,
|
||||
"endTimestamp": endTimestamp})
|
||||
cur.execute(query=query)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react'
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
|
|
@ -10,7 +11,7 @@ function CustomMetriPercentage(props: Props) {
|
|||
const { data = {} } = props;
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center" style={{ height: '240px'}}>
|
||||
<div className="text-6xl">{data.count}</div>
|
||||
<div className="text-6xl">{numberWithCommas(data.count)}</div>
|
||||
<div className="text-lg mt-6">{`${data.previousCount} ( ${data.countProgress}% )`}</div>
|
||||
<div className="color-gray-medium">from previous period.</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { PieChart, Pie, Cell } from 'recharts';
|
|||
import { Styles } from '../../common';
|
||||
import { NoContent } from 'UI';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
interface Props {
|
||||
metric: any,
|
||||
data: any;
|
||||
|
|
@ -107,7 +108,7 @@ function CustomMetricPieChart(props: Props) {
|
|||
dominantBaseline="central"
|
||||
fill='#666'
|
||||
>
|
||||
{name || 'Unidentified'} {value}
|
||||
{name || 'Unidentified'} {numberWithCommas(value)}
|
||||
</text>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { List } from 'immutable';
|
|||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import { NoContent } from 'UI';
|
||||
import { tableColumnName } from 'App/constants/filterOptions';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
const getColumns = (metric) => {
|
||||
return [
|
||||
|
|
@ -16,7 +17,7 @@ const getColumns = (metric) => {
|
|||
{
|
||||
key: 'sessionCount',
|
||||
title: 'Sessions',
|
||||
toText: sessions => sessions,
|
||||
toText: sessions => numberWithCommas(sessions),
|
||||
width: '30%',
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default class Table extends React.PureComponent {
|
|||
}
|
||||
</div>
|
||||
<div className={ cn(stl.content, "thin-scrollbar") } style={{ maxHeight: maxHeight + 'px'}}>
|
||||
{ rows.take(showAll ? 10 : (small ? 3 : 5)).map(row => (
|
||||
{ rows.take(showAll ? rows.size : (small ? 3 : 5)).map(row => (
|
||||
<div
|
||||
className={ cn(rowClass, stl.row, { [stl.small]: small, 'cursor-pointer' : !!onRowClick}) }
|
||||
key={ row.key }
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ const FunnelDetails = (props) => {
|
|||
const showEmptyMessage = hasNoStages && activeTab === TAB_ISSUES && !loading;
|
||||
|
||||
return (
|
||||
<div className="page-margin container-70" >
|
||||
<div className="page-margin container-70">
|
||||
<FunnelHeader
|
||||
funnel={funnel}
|
||||
insights={insights}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Icon, BackLink, IconButton, Dropdown, Popup, TextEllipsis, Button } from 'UI';
|
||||
import { remove as deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered } from 'Duck/funnels';
|
||||
import { applyFilter } from 'Duck/funnelFilters';
|
||||
import { editFilter, addFilter } from 'Duck/funnels';
|
||||
import DateRange from 'Shared/DateRange';
|
||||
import { connect } from 'react-redux';
|
||||
import { confirm } from 'UI/Confirmation';
|
||||
|
|
@ -18,23 +18,18 @@ const Info = ({ label = '', value = '', className = 'mx-4' }) => {
|
|||
}
|
||||
|
||||
const FunnelHeader = (props) => {
|
||||
const { funnelFilters, funnel, insights, funnels, onBack, funnelId, showFilters = false, renameHandler } = props;
|
||||
const { funnel, insights, funnels, onBack, funnelId, showFilters = false, renameHandler } = props;
|
||||
|
||||
const [showSaveModal, setShowSaveModal] = useState(false)
|
||||
|
||||
const writeOption = (e, { name, value }) => {
|
||||
const writeOption = (e, { name, value }) => {
|
||||
props.fetch(value)
|
||||
props.fetchInsights(value, {})
|
||||
props.fetchIssuesFiltered(value, {})
|
||||
props.fetchSessionsFiltered(value, {})
|
||||
props.redirect(value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (funnel.funnelId && funnel.funnelId !== funnelId) {
|
||||
props.fetchInsights(funnel.funnelId, {})
|
||||
props.fetchIssuesFiltered(funnel.funnelId, {})
|
||||
props.fetchSessionsFiltered(funnel.funnelId, {})
|
||||
}
|
||||
}, [funnel])
|
||||
|
||||
const deleteFunnel = async (e, funnel) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -48,12 +43,12 @@ const FunnelHeader = (props) => {
|
|||
} else {}
|
||||
}
|
||||
|
||||
const onDateChange = (e) => {
|
||||
props.applyFilter(e, funnel.funnelId)
|
||||
const onDateChange = (e) => {
|
||||
props.editFilter(e, funnel.funnelId);
|
||||
}
|
||||
|
||||
const options = funnels.map(({ funnelId, name }) => ({ text: name, value: funnelId })).toJS();
|
||||
const selectedFunnel = funnels.filter(i => i.funnelId === parseInt(funnelId)).first() || {};
|
||||
const selectedFunnel = funnels.filter(i => i.funnelId === parseInt(funnelId)).first() || {};
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -75,7 +70,7 @@ const FunnelHeader = (props) => {
|
|||
className={ stl.dropdown }
|
||||
name="funnel"
|
||||
value={ parseInt(funnelId) }
|
||||
icon={null}
|
||||
// icon={null}
|
||||
onChange={ writeOption }
|
||||
selectOnBlur={false}
|
||||
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
|
||||
|
|
@ -104,9 +99,9 @@ const FunnelHeader = (props) => {
|
|||
/>
|
||||
</div>
|
||||
<DateRange
|
||||
rangeValue={funnelFilters.rangeValue}
|
||||
startDate={funnelFilters.startDate}
|
||||
endDate={funnelFilters.endDate}
|
||||
rangeValue={funnel.filter.rangeValue}
|
||||
startDate={funnel.filter.startDate}
|
||||
endDate={funnel.filter.endDate}
|
||||
onDateChange={onDateChange}
|
||||
customRangeRight
|
||||
/>
|
||||
|
|
@ -117,5 +112,5 @@ const FunnelHeader = (props) => {
|
|||
}
|
||||
|
||||
export default connect(state => ({
|
||||
funnelFilters: state.getIn([ 'funnels', 'instance', 'filter' ]),
|
||||
}), { applyFilter, deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered })(FunnelHeader)
|
||||
funnel: state.getIn([ 'funnels', 'instance' ]),
|
||||
}), { editFilter, deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered })(FunnelHeader)
|
||||
|
|
|
|||
|
|
@ -15,32 +15,40 @@ const inputModeOptions = [
|
|||
{ text: 'Obscure all inputs', value: 'hidden' },
|
||||
];
|
||||
|
||||
const codeSnippet = `<!-- OpenReplay Tracking Code for HOST -->
|
||||
<script>
|
||||
var initOpts = { projectKey: "PROJECT_KEY", ingestPoint: "https://${window.location.hostname}/ingest"};
|
||||
var startOpts = { userID: "" };
|
||||
(function(A,s,a,y,e,r){
|
||||
r=window.OpenReplay=[e,r,y,[s-1, e]];
|
||||
s=document.createElement('script');s.src=A;s.async=!a;
|
||||
document.getElementsByTagName('head')[0].appendChild(s);
|
||||
r.start=function(v){r.push([0])};
|
||||
r.stop=function(v){r.push([1])};
|
||||
r.setUserID=function(id){r.push([2,id])};
|
||||
r.setUserAnonymousID=function(id){r.push([3,id])};
|
||||
r.setMetadata=function(k,v){r.push([4,k,v])};
|
||||
r.event=function(k,p,i){r.push([5,k,p,i])};
|
||||
r.issue=function(k,p){r.push([6,k,p])};
|
||||
r.isActive=function(){return false};
|
||||
r.getSessionToken=function(){};
|
||||
})("//static.openreplay.com/${window.ENV.TRACKER_VERSION}/openreplay.js",XXX,0,initOpts,startOpts);
|
||||
</script>`;
|
||||
|
||||
const inputModeOptionsMap = {}
|
||||
inputModeOptions.forEach((o, i) => inputModeOptionsMap[o.value] = i)
|
||||
|
||||
const ProjectCodeSnippet = props => {
|
||||
const { site, gdpr } = props;
|
||||
const [changed, setChanged] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const codeSnippet = `<!-- OpenReplay Tracking Code for HOST -->
|
||||
<script>
|
||||
var initOpts = {
|
||||
projectKey: "PROJECT_KEY",
|
||||
ingestPoint: "https://${window.location.hostname}/ingest",
|
||||
defaultInputMode: ${inputModeOptionsMap[gdpr.defaultInputMode]},
|
||||
obscureTextNumbers: ${gdpr.maskNumbers},
|
||||
obscureTextEmails: ${gdpr.maskEmails},
|
||||
};
|
||||
var startOpts = { userID: "" };
|
||||
(function(A,s,a,y,e,r){
|
||||
r=window.OpenReplay=[e,r,y,[s-1, e]];
|
||||
s=document.createElement('script');s.src=A;s.async=!a;
|
||||
document.getElementsByTagName('head')[0].appendChild(s);
|
||||
r.start=function(v){r.push([0])};
|
||||
r.stop=function(v){r.push([1])};
|
||||
r.setUserID=function(id){r.push([2,id])};
|
||||
r.setUserAnonymousID=function(id){r.push([3,id])};
|
||||
r.setMetadata=function(k,v){r.push([4,k,v])};
|
||||
r.event=function(k,p,i){r.push([5,k,p,i])};
|
||||
r.issue=function(k,p){r.push([6,k,p])};
|
||||
r.isActive=function(){return false};
|
||||
r.getSessionToken=function(){};
|
||||
})("//static.openreplay.com/${window.ENV.TRACKER_VERSION}/openreplay.js",1,0,initOpts,startOpts);
|
||||
</script>`;
|
||||
|
||||
const saveGDPR = (value) => {
|
||||
setChanged(true)
|
||||
props.saveGDPR(site.id, GDPR({...value}));
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
import React, { useState } from 'react';
|
||||
import { IconButton } from 'UI';
|
||||
import FunnelSaveModal from 'App/components/Funnels/FunnelSaveModal';
|
||||
|
||||
export default function SaveFunnelButton() {
|
||||
import { connect } from 'react-redux';
|
||||
import { init } from 'Duck/funnels';
|
||||
interface Props {
|
||||
filter: any
|
||||
init: (instance: any) => void
|
||||
}
|
||||
function SaveFunnelButton(props: Props) {
|
||||
const [showModal, setshowModal] = useState(false)
|
||||
|
||||
const handleClick = () => {
|
||||
props.init({ filter: props.filter })
|
||||
setshowModal(true)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
className="mr-2"
|
||||
onClick={() => setshowModal(true)} primaryText label="SAVE FUNNEL" icon="funnel"
|
||||
onClick={handleClick} primaryText label="SAVE FUNNEL" icon="funnel"
|
||||
/>
|
||||
|
||||
<FunnelSaveModal
|
||||
|
|
@ -18,3 +28,7 @@ export default function SaveFunnelButton() {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
filter: state.getIn(['search', 'instance']),
|
||||
}), { init })(SaveFunnelButton);
|
||||
|
|
@ -14,25 +14,8 @@ const inputModeOptions = [
|
|||
{ text: 'Obscure all inputs', value: 'hidden' },
|
||||
];
|
||||
|
||||
const codeSnippet = `<!-- OpenReplay Tracking Code for HOST -->
|
||||
<script>
|
||||
var initOpts = { projectKey: "PROJECT_KEY", ingestPoint: "https://${window.location.hostname}/ingest"};
|
||||
var startOpts = { userID: "" };
|
||||
(function(A,s,a,y,e,r){
|
||||
r=window.OpenReplay=[e,r,y,[s-1, e]];
|
||||
s=document.createElement('script');s.src=A;s.async=!a;
|
||||
document.getElementsByTagName('head')[0].appendChild(s);
|
||||
r.start=function(v){r.push([0])};
|
||||
r.stop=function(v){r.push([1])};
|
||||
r.setUserID=function(id){r.push([2,id])};
|
||||
r.setUserAnonymousID=function(id){r.push([3,id])};
|
||||
r.setMetadata=function(k,v){r.push([4,k,v])};
|
||||
r.event=function(k,p,i){r.push([5,k,p,i])};
|
||||
r.issue=function(k,p){r.push([6,k,p])};
|
||||
r.isActive=function(){return false};
|
||||
r.getSessionToken=function(){};
|
||||
})("//static.openreplay.com/${window.ENV.TRACKER_VERSION}/openreplay.js",XXX,0,initOpts,startOpts);
|
||||
</script>`;
|
||||
const inputModeOptionsMap = {}
|
||||
inputModeOptions.forEach((o, i) => inputModeOptionsMap[o.value] = i)
|
||||
|
||||
|
||||
const ProjectCodeSnippet = props => {
|
||||
|
|
@ -40,6 +23,32 @@ const ProjectCodeSnippet = props => {
|
|||
const [changed, setChanged] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const codeSnippet = `<!-- OpenReplay Tracking Code for HOST -->
|
||||
<script>
|
||||
var initOpts = {
|
||||
projectKey: "PROJECT_KEY",
|
||||
ingestPoint: "https://${window.location.hostname}/ingest",
|
||||
defaultInputMode: ${inputModeOptionsMap[gdpr.defaultInputMode]},
|
||||
obscureTextNumbers: ${gdpr.maskNumbers},
|
||||
obscureTextEmails: ${gdpr.maskEmails},
|
||||
};
|
||||
var startOpts = { userID: "" };
|
||||
(function(A,s,a,y,e,r){
|
||||
r=window.OpenReplay=[e,r,y,[s-1, e]];
|
||||
s=document.createElement('script');s.src=A;s.async=!a;
|
||||
document.getElementsByTagName('head')[0].appendChild(s);
|
||||
r.start=function(v){r.push([0])};
|
||||
r.stop=function(v){r.push([1])};
|
||||
r.setUserID=function(id){r.push([2,id])};
|
||||
r.setUserAnonymousID=function(id){r.push([3,id])};
|
||||
r.setMetadata=function(k,v){r.push([4,k,v])};
|
||||
r.event=function(k,p,i){r.push([5,k,p,i])};
|
||||
r.issue=function(k,p){r.push([6,k,p])};
|
||||
r.isActive=function(){return false};
|
||||
r.getSessionToken=function(){};
|
||||
})("//static.openreplay.com/${window.ENV.TRACKER_VERSION}/openreplay.js",1,0,initOpts,startOpts);
|
||||
</script>`;
|
||||
|
||||
const saveGDPR = (value) => {
|
||||
setChanged(true)
|
||||
props.saveGDPR(site.id, GDPR({...value}));
|
||||
|
|
@ -47,11 +56,11 @@ const ProjectCodeSnippet = props => {
|
|||
|
||||
const onChangeSelect = (event, { name, value }) => {
|
||||
const { gdpr } = site;
|
||||
const _gdpr = { ...gdpr.toData() };
|
||||
// const _gdpr = { ...gdpr.toData() };
|
||||
// props.editGDPR({ [ name ]: value });
|
||||
// _gdpr[name] = value;
|
||||
props.editGDPR({ [ name ]: value });
|
||||
_gdpr[name] = value;
|
||||
props.editGDPR({ [ name ]: value });
|
||||
saveGDPR(_gdpr)
|
||||
saveGDPR({ ...gdpr, [ name ]: value });
|
||||
};
|
||||
|
||||
const onChangeOption = (event, { name, checked }) => {
|
||||
|
|
@ -74,8 +83,8 @@ const ProjectCodeSnippet = props => {
|
|||
snippet = snippet.replace('PROJECT_KEY', site.projectKey);
|
||||
}
|
||||
return snippet
|
||||
.replace('XXX', getOptionValues())
|
||||
.replace('HOST', site && site.host);
|
||||
//.replace('XXX', getOptionValues())
|
||||
//.replace('HOST', site && site.host);
|
||||
}
|
||||
|
||||
const copyHandler = (code) => {
|
||||
|
|
|
|||
|
|
@ -74,10 +74,10 @@ export const metricOf = [
|
|||
{ text: 'Session Count', value: 'sessionCount', type: 'timeseries' },
|
||||
{ text: 'Users', value: FilterKey.USERID, type: 'table' },
|
||||
{ text: 'Issues', value: FilterKey.ISSUE, type: 'table' },
|
||||
{ text: 'Browser', value: FilterKey.USER_BROWSER, type: 'table' },
|
||||
{ text: 'Device', value: FilterKey.USER_DEVICE, type: 'table' },
|
||||
{ text: 'Country', value: FilterKey.USER_COUNTRY, type: 'table' },
|
||||
{ text: 'URL', value: FilterKey.LOCATION, type: 'table' },
|
||||
{ text: 'Browsers', value: FilterKey.USER_BROWSER, type: 'table' },
|
||||
{ text: 'Devices', value: FilterKey.USER_DEVICE, type: 'table' },
|
||||
{ text: 'Countries', value: FilterKey.USER_COUNTRY, type: 'table' },
|
||||
{ text: 'URLs', value: FilterKey.LOCATION, type: 'table' },
|
||||
]
|
||||
|
||||
export const methodOptions = [
|
||||
|
|
|
|||
|
|
@ -112,9 +112,9 @@ const reducer = (state = initialState, action = {}) => {
|
|||
return action.fromUrl
|
||||
? state.set('appliedFilter',
|
||||
Filter(action.filter)
|
||||
.set('events', state.getIn([ 'appliedFilter', 'events' ]))
|
||||
// .set('events', state.getIn([ 'appliedFilter', 'events' ]))
|
||||
)
|
||||
: state.mergeIn([ 'appliedFilter' ], action.filter);
|
||||
: state.mergeIn(['instance', 'filter'], action.filter);
|
||||
case ADD_CUSTOM_FILTER:
|
||||
return state.update('customFilters', vars => vars.set(action.filter, action.value));
|
||||
case REMOVE_CUSTOM_FILTER:
|
||||
|
|
|
|||
|
|
@ -43,10 +43,11 @@ const reducer = (state = initialState, action = {}) => {
|
|||
return state.mergeIn([ 'instance', 'gdpr' ], action.data);
|
||||
case SAVE_GDPR_SUCCESS:
|
||||
const gdpr = GDPR(action.data);
|
||||
return state.update('list', itemInListUpdater({
|
||||
[ idKey ] : state.getIn([ 'instance', idKey ]),
|
||||
gdpr,
|
||||
})).setIn([ 'instance', 'gdpr' ], gdpr);
|
||||
return state.setIn([ 'instance', 'gdpr' ], gdpr);
|
||||
// return state.update('list', itemInListUpdater({
|
||||
// [ idKey ] : state.getIn([ 'instance', idKey ]),
|
||||
// gdpr,
|
||||
// })).setIn([ 'instance', 'gdpr' ], gdpr);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export enum FilterKey {
|
|||
AVG_CPU_LOAD = "AVG_CPU_LOAD",
|
||||
AVG_MEMORY_USAGE = "AVG_MEMORY_USAGE",
|
||||
FETCH_FAILED = "FETCH_FAILED",
|
||||
|
||||
FETCH = "FETCH",
|
||||
FETCH_URL = "FETCH_URL",
|
||||
FETCH_STATUS_CODE = "FETCH_STATUS_CODE",
|
||||
|
|
@ -70,4 +71,9 @@ export enum FilterKey {
|
|||
FETCH_DURATION = "FETCH_DURATION",
|
||||
FETCH_REQUEST_BODY = "FETCH_REQUEST_BODY",
|
||||
FETCH_RESPONSE_BODY = "FETCH_RESPONSE_BODY",
|
||||
|
||||
GRAPHQL_NAME = "GRAPHQL_NAME",
|
||||
GRAPHQL_METHOD = "GRAPHQL_METHOD",
|
||||
GRAPHQL_REQUEST_BODY = "GRAPHQL_REQUEST_BODY",
|
||||
GRAPHQL_RESPONSE_BODY = "GRAPHQL_RESPONSE_BODY",
|
||||
}
|
||||
|
|
@ -12,8 +12,21 @@ export const filtersMap = {
|
|||
[FilterKey.INPUT]: { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
|
||||
[FilterKey.LOCATION]: { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Page', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
|
||||
[FilterKey.CUSTOM]: { key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true },
|
||||
[FilterKey.REQUEST]: { key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
|
||||
[FilterKey.GRAPHQL]: { key: FilterKey.GRAPHQL, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'GraphQL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/graphql', isEvent: true },
|
||||
// [FilterKey.REQUEST]: { key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
|
||||
[FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, operator: 'is', label: 'Network Request', filters: [
|
||||
{ key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_STATUS_CODE, type: FilterType.NUMBER_MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
|
||||
{ key: FilterKey.FETCH_DURATION, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'with duration', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
], icon: 'filters/fetch', isEvent: true },
|
||||
[FilterKey.GRAPHQL]: { key: FilterKey.GRAPHQL, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, label: 'GraphQL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/graphql', isEvent: true, filters: [
|
||||
{ key: FilterKey.GRAPHQL_NAME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with name', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.GRAPHQL_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
|
||||
{ key: FilterKey.GRAPHQL_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.GRAPHQL_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
]},
|
||||
[FilterKey.STATEACTION]: { key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'StateAction', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
|
||||
[FilterKey.ERROR]: { key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
|
||||
// [FilterKey.METADATA]: { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
|
||||
|
|
@ -33,14 +46,6 @@ export const filtersMap = {
|
|||
[FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
|
||||
|
||||
// PERFORMANCE
|
||||
[FilterKey.FETCH]: { key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.PERFORMANCE, operator: 'is', label: 'Network Request', filters: [
|
||||
{ key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_STATUS_CODE, type: FilterType.NUMBER_MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', options: filterOptions.methodOptions },
|
||||
{ key: FilterKey.FETCH_DURATION, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'with duration', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
], icon: 'filters/perfromance-network-request', isEvent: true },
|
||||
[FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
[FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
[FilterKey.TTFB]: { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const getUniqueFilter = keys =>
|
|||
!list.some((item2, j) => j < i &&
|
||||
keys.every(key => item[ key ] === item2[ key ] && item[ key ] !== undefined));
|
||||
|
||||
export const numberWithCommas = (x) => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
export const numberWithCommas = (x) => x ? x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : 0;
|
||||
|
||||
export const numberCompact = (x) => x >= 1000 ? x / 1000 + 'k': x;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue