diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..78cc7c8f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Report an issue and help improve OpenReplay +title: '' +labels: bug +assignees: estradino + +--- + +**Describe the issue** +A short description of what the issue is. + +**Steps to reproduce the issue** +1. Step 1 +2. Step 2 +3. You got it :) + +**Expected behavior** +What you expected to happen. + +**Screenshots** +If possible, that would be make our life easier. + +**OpenReplay Environment** + - Frontend stack: [e.g. React/Axios/MobX, Next] + - OpenReplay version: [e.g. 1.6.0] + - Tracker version: [e.g. 3.5.10] + - Plugins used: [e.g. Fetch, Redux] + - Cloud provider: [e.g. AWS, GCP] + - System specs: [e.g. 2vCPU/16Gb with 50Gb of storage] + +**Additional context** +Add additional information you think might be relevant for this behavior. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..23ecbd8cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: true +contact_links: + - name: Documentation Request + url: https://github.com/openreplay/documentation/issues + about: Report a mistake or suggest anything we might be missing in the docs + - name: Discussions + url: https://github.com/openreplay/openreplay/discussions + about: Ask and answer various questions on GitHub Discussions + - name: Join our Slack Community + url: https://slack.openreplay.com + about: Take the discussion further by joining our community on Slack diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..7121b13e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,10 @@ +--- +name: Feature request +about: Suggest an idea or a feature to improve OpenReplay +title: '' +labels: feature-request +assignees: estradino + +--- + +Briefly describe the feature you would like to see shipped with the upcoming versions of OpenReplay, the use-case (very important to us) and the alternative solutions you've considered so far. diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml index 305bb094a..c664a1102 100644 --- a/.github/workflows/api.yaml +++ b/.github/workflows/api.yaml @@ -4,7 +4,6 @@ on: push: branches: - dev - - api-v1.5.5 paths: - api/** diff --git a/README.md b/README.md index 870d47fcc..0f9f35669 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@

-OpenReplay is a session replay stack that lets you see what users do on your web app, helping you troubleshoot issues faster. It's the only open-source alternative to products such as FullStory and LogRocket. +OpenReplay is a session replay suite you can host yourself, that lets you see what users do on your web app, helping you troubleshoot issues faster. It's the only open-source alternative to products such as FullStory and LogRocket. - **Session replay.** OpenReplay replays what users do, but not only. It also shows you what went under the hood, how your website or app behaves by capturing network activity, console logs, JS errors, store actions/state, page speed metrics, cpu/memory usage and much more. - **Low footprint**. With a ~18KB (.gz) tracker that asynchronously sends minimal data for a very limited impact on performance. @@ -59,6 +59,7 @@ OpenReplay can be deployed anywhere. Follow our step-by-step guides for deployin - [Azure](https://docs.openreplay.com/deployment/deploy-azure) - [Digital Ocean](https://docs.openreplay.com/deployment/deploy-digitalocean) - [Scaleway](https://docs.openreplay.com/deployment/deploy-scaleway) +- [OVHcloud](https://docs.openreplay.com/deployment/deploy-ovhcloud) - [Kubernetes](https://docs.openreplay.com/deployment/deploy-kubernetes) ## OpenReplay Cloud @@ -69,7 +70,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: -- [Discord](https://discord.openreplay.com) (Connect with our engineers and community) +- [Slack](https://slack.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 +81,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 [Discord](https://discord.openreplay.com) to ask questions, discuss ideas or connect with our contributors. +Also, feel free to join our [Slack](https://slack.openreplay.com) to ask questions, discuss ideas or connect with our contributors. ## Roadmap @@ -89,3 +90,9 @@ Check out our [roadmap](https://www.notion.so/openreplay/Roadmap-889d2c3d968b478 ## License This repo is under the Elastic License 2.0 (ELv2), with the exception of the `ee` directory. + +## Contributors + + + + diff --git a/backend/internal/assets/cacher/cacher.go b/backend/internal/assets/cacher/cacher.go index 752241e37..fd7fe1e70 100644 --- a/backend/internal/assets/cacher/cacher.go +++ b/backend/internal/assets/cacher/cacher.go @@ -111,7 +111,7 @@ func (c *cacher) cacheURL(requestURL string, sessionID uint64, depth byte, urlCo strData := string(data) if isCSS { - strData = c.rewriter.RewriteCSS(sessionID, requestURL, strData) // TODO: one method for reqrite and return list + strData = c.rewriter.RewriteCSS(sessionID, requestURL, strData) // TODO: one method for rewrite and return list } // TODO: implement in streams diff --git a/backend/services/db/messages.go b/backend/services/db/messages.go new file mode 100644 index 000000000..e69de29bb diff --git a/ee/LICENSE.md b/ee/LICENSE.md index 5f6043f8f..ed992009e 100644 --- a/ee/LICENSE.md +++ b/ee/LICENSE.md @@ -1,4 +1,4 @@ The OpenReplay Enterprise license (the “Enterprise License”) -Copyright (c) 2022 Asayer SAS. +Copyright (c) 2022 Asayer, Inc. To license the Enterprise Edition of OpenReplay, and take advantage of its additional features, functionality and support, you must agree to the terms of the OpenReplay Enterprise License Agreement. Please contact OpenReplay at [sales@openreplay.com](mailto:sales@openreplay.com). diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index 10404a6e2..d3a63e49a 100644 --- a/frontend/app/components/BugFinder/BugFinder.js +++ b/frontend/app/components/BugFinder/BugFinder.js @@ -17,7 +17,8 @@ import SessionsMenu from './SessionsMenu/SessionsMenu'; import NoSessionsMessage from 'Shared/NoSessionsMessage'; import SessionSearch from 'Shared/SessionSearch'; import MainSearchBar from 'Shared/MainSearchBar'; -import { clearSearch, fetchSessions } from 'Duck/search'; +import { clearSearch, fetchSessions, addFilterByKeyAndValue } from 'Duck/search'; +import { FilterKey } from 'Types/filter/filterType'; const weakEqual = (val1, val2) => { if (!!val1 === false && !!val2 === false) return true; @@ -62,6 +63,7 @@ const allowedQueryKeys = [ setActiveTab, clearSearch, fetchSessions, + addFilterByKeyAndValue, }) @withPageTitle("Sessions - OpenReplay") export default class BugFinder extends React.PureComponent { @@ -88,7 +90,11 @@ export default class BugFinder extends React.PureComponent { const queryFilter = this.props.query.all(allowedQueryKeys); if (queryFilter.hasOwnProperty('userId')) { - props.addAttribute({ label: 'User Id', key: KEYS.USERID, type: KEYS.USERID, operator: 'is', value: queryFilter.userId }) + props.addFilterByKeyAndValue(FilterKey.USERID, queryFilter.userId); + } else { + if (props.sessions.size === 0) { + props.fetchSessions(); + } } } diff --git a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx index e1e2ccd06..d612efe0b 100644 --- a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx +++ b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx @@ -4,6 +4,7 @@ import { useObserver } from 'mobx-react-lite'; import { Icon } from 'UI'; import cn from 'classnames'; import { useStore } from 'App/mstore'; +import { Loader } from 'UI'; function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds }) { const selectedCategoryWidgetsCount = useObserver(() => { @@ -32,7 +33,8 @@ interface IProps { function DashboardMetricSelection(props: IProps) { const { dashboardStore } = useStore(); - const widgetCategories: any[] = useObserver(() => dashboardStore.widgetCategories); + let widgetCategories: any[] = useObserver(() => dashboardStore.widgetCategories); + const loadingTemplates = useObserver(() => dashboardStore.loadingTemplates); const [activeCategory, setActiveCategory] = React.useState(); const [selectAllCheck, setSelectAllCheck] = React.useState(false); const selectedWidgetIds = useObserver(() => dashboardStore.selectedWidgets.map((widget: any) => widget.metricId)); @@ -65,7 +67,7 @@ function DashboardMetricSelection(props: IProps) { } return useObserver(() => ( -
+
Type
@@ -137,7 +139,7 @@ function DashboardMetricSelection(props: IProps) {
- + )); } diff --git a/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx b/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx index 0250da735..b8efa4406 100644 --- a/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx +++ b/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx @@ -6,7 +6,7 @@ import { Button } from 'UI'; import { withRouter } from 'react-router-dom'; import { useStore } from 'App/mstore'; import { useModal } from 'App/components/Modal'; -import { dashboardMetricCreate, withSiteId } from 'App/routes'; +import { dashboardMetricCreate, withSiteId, dashboardSelected } from 'App/routes'; interface Props { history: any @@ -19,13 +19,14 @@ function DashboardModal(props) { const { dashboardStore } = useStore(); const selectedWidgetsCount = useObserver(() => dashboardStore.selectedWidgets.length); const { hideModal } = useModal(); + const loadingTemplates = useObserver(() => dashboardStore.loadingTemplates); const dashboard = useObserver(() => dashboardStore.dashboardInstance); const loading = useObserver(() => dashboardStore.isSaving); const onSave = () => { dashboardStore.save(dashboard).then(async (syncedDashboard) => { if (dashboard.exists()) { - await dashboardStore.fetch(dashboard.dashboardId) + await dashboardStore.fetch(dashboard.dashboardId) } dashboardStore.selectDashboardById(syncedDashboard.dashboardId); history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId)) @@ -45,7 +46,7 @@ function DashboardModal(props) {
diff --git a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx index f08fafd61..a9b6e2046 100644 --- a/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx +++ b/frontend/app/components/Dashboard/components/WidgetWrapper/WidgetWrapper.tsx @@ -88,7 +88,7 @@ function WidgetWrapper(props: Props & RouteComponentProps) { onClick={props.onClick ? props.onClick : () => {}} id={`widget-${widget.widgetId}`} > - {!isTemplate && isWidget && isPredefined && + {!isTemplate && isWidget && isPredefined &&
{ @@ -146,6 +144,7 @@ export default class List extends React.PureComponent { total, sort, order, + limit, } = this.props; const { checkedAll, @@ -234,9 +233,9 @@ export default class List extends React.PureComponent { > { list.map(e => -
+
this.props.updateCurrentPage(page)} - limit={PER_PAGE} + limit={limit} debounceRequest={500} />
diff --git a/frontend/app/components/Header/SiteDropdown.js b/frontend/app/components/Header/SiteDropdown.js index d6df6be31..9b3af1f8f 100644 --- a/frontend/app/components/Header/SiteDropdown.js +++ b/frontend/app/components/Header/SiteDropdown.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { setSiteId } from 'Duck/site'; import { withRouter } from 'react-router-dom'; -import { hasSiteId, siteChangeAvaliable } from 'App/routes'; +import { hasSiteId, siteChangeAvaliable, isRoute } from 'App/routes'; import { STATUS_COLOR_MAP, GREEN } from 'Types/site'; import { Icon, SlideModal } from 'UI'; import { pushNewSite } from 'Duck/user' @@ -42,10 +42,10 @@ export default class SiteDropdown extends React.PureComponent { } switchSite = (siteId) => { - const { mstore } = this.props + const { mstore, location } = this.props this.props.setSiteId(siteId); - this.props.clearSearch(); + this.props.clearSearch(location.pathname.includes('/sessions')); this.props.fetchIntegrationVariables(); mstore.initClient(); @@ -59,7 +59,7 @@ export default class SiteDropdown extends React.PureComponent { const disabled = !siteChangeAvaliable(pathname); const showCurrent = hasSiteId(pathname) || siteChangeAvaliable(pathname); // const canAddSites = isAdmin && account.limits.projects && account.limits.projects.remaining !== 0; - + return (
{ diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/InstallDocs/InstallDocs.js b/frontend/app/components/Onboarding/components/OnboardingTabs/InstallDocs/InstallDocs.js index 5096f0b16..a89904907 100644 --- a/frontend/app/components/Onboarding/components/OnboardingTabs/InstallDocs/InstallDocs.js +++ b/frontend/app/components/Onboarding/components/OnboardingTabs/InstallDocs/InstallDocs.js @@ -30,7 +30,7 @@ function MyApp() { //... }` -function InstallDocs({ siteId, sites }) { +function InstallDocs({ siteId, sites }) { const site = sites.find(s => s.id === siteId); const _usageCode = usageCode.replace('PROJECT_KEY', site.projectKey) const _usageCodeSST = usageCodeSST.replace('PROJECT_KEY', site.projectKey) diff --git a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx index 190cbfef6..99614b32a 100644 --- a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx +++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx @@ -137,7 +137,7 @@ function LiveSessionList(props: Props) { show={ !loading && list.size === 0} >
- + {list.map(session => ( <> e.get("errorId") === action.id)) { return updateItemInList(state, { errorId: action.data.errorId, viewed: true }) @@ -73,15 +74,15 @@ function reducer(state = initialState, action = {}) { .set("totalCount", data ? data.total : 0) .set("list", List(data && data.errors).map(ErrorInfo) .filter(e => e.parentErrorId == null) - .map(e => e.update("chart", chartWrapper))); + .map(e => e.update("chart", chartWrapper))) case success(RESOLVE): - updError = { errorId: action.id, status: RESOLVED }; + updError = { errorId: action.id, status: RESOLVED, disabled: true }; return updateItemInList(updateInstance(state, updError), updError); case success(UNRESOLVE): - updError = { errorId: action.id, status: UNRESOLVED }; + updError = { errorId: action.id, status: UNRESOLVED, disabled: true }; return updateItemInList(updateInstance(state, updError), updError); case success(IGNORE): - updError = { errorId: action.id, status: IGNORED }; + updError = { errorId: action.id, status: IGNORED, disabled: true }; return updateItemInList(updateInstance(state, updError), updError); case success(TOGGLE_FAVORITE): return state.mergeIn([ "instance" ], { favorite: !state.getIn([ "instance", "favorite" ]) }) @@ -163,28 +164,43 @@ export function fetchBookmarks() { } } -export function resolve(id) { - return { +export const resolve = (id) => (dispatch, getState) => { + const list = getState().getIn(['errors', 'list']); + const index = list.findIndex(e => e.get('errorId') === id); + const error = list.get(index); + if (error.get('status') === RESOLVED) return; + + return dispatch({ types: array(RESOLVE), id, call: client => client.get(`/errors/${ id }/solve`), - } + }) } -export function unresolve(id) { - return { +export const unresolve = (id) => (dispatch, getState) => { + const list = getState().getIn(['errors', 'list']); + const index = list.findIndex(e => e.get('errorId') === id); + const error = list.get(index); + if (error.get('status') === UNRESOLVED) return; + + return dispatch({ types: array(UNRESOLVE), id, call: client => client.get(`/errors/${ id }/unsolve`), - } + }) } -export function ignore(id) { - return { +export const ignore = (id) => (dispatch, getState) => { + const list = getState().getIn(['errors', 'list']); + const index = list.findIndex(e => e.get('errorId') === id); + const error = list.get(index); + if (error.get('status') === IGNORED) return; + + return dispatch({ types: array(IGNORE), id, call: client => client.get(`/errors/${ id }/ignore`), - } + }) } export function merge(ids) { diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js index 525e3b005..c49b00b26 100644 --- a/frontend/app/duck/search.js +++ b/frontend/app/duck/search.js @@ -180,6 +180,11 @@ export const edit = reduceThenFetchResource((instance) => ({ instance, })); +export const editDefault = (instance) => ({ + type: EDIT, + instance, +}); + export const setActiveTab = reduceThenFetchResource((tab) => ({ type: SET_ACTIVE_TAB, tab, diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index 0a1aca2b3..c4e739fd8 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -108,9 +108,9 @@ export default class DashboardStore implements IDashboardSotre { isLoading: boolean = true; isSaving: boolean = false; isDeleting: boolean = false; + loadingTemplates: boolean = false fetchingDashboard: boolean = false; sessionsLoading: boolean = false; - showAlertModal: boolean = false; constructor() { @@ -379,6 +379,7 @@ export default class DashboardStore implements IDashboardSotre { }; fetchTemplates(hardRefresh): Promise { + this.loadingTemplates = true return new Promise((resolve, reject) => { if (this.widgetCategories.length > 0 && !hardRefresh) { resolve(this.widgetCategories); @@ -389,11 +390,7 @@ export default class DashboardStore implements IDashboardSotre { const categories: any[] = []; response.forEach((category: any) => { const widgets: any[] = []; - // TODO speed_location is not supported yet category.widgets - .filter( - (w: any) => w.predefinedKey !== "speed_locations" - ) .forEach((widget: any) => { const w = new Widget().fromJson(widget); widgets.push(w); @@ -409,6 +406,8 @@ export default class DashboardStore implements IDashboardSotre { }) .catch((error) => { reject(error); + }).finally(() => { + this.loadingTemplates = false }); } }); diff --git a/frontend/app/mstore/types/filterItem.ts b/frontend/app/mstore/types/filterItem.ts index 99d91c25a..ce2a1f4db 100644 --- a/frontend/app/mstore/types/filterItem.ts +++ b/frontend/app/mstore/types/filterItem.ts @@ -30,7 +30,11 @@ export default class FilterItem { merge: action }) - + if (Array.isArray(data.filters)) { + data.filters = data.filters.map(function (i) { + return new FilterItem(i); + }); + } this.merge(data) } diff --git a/frontend/app/player/MessageDistributor/managers/DOMManager.ts b/frontend/app/player/MessageDistributor/managers/DOMManager.ts index 9f1760df8..43c7a274c 100644 --- a/frontend/app/player/MessageDistributor/managers/DOMManager.ts +++ b/frontend/app/player/MessageDistributor/managers/DOMManager.ts @@ -209,9 +209,9 @@ export default class DOMManager extends ListWalker { case "set_input_value": node = this.nl[ msg.id ] if (!node) { logger.error("Node not found", msg); return } - if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement)) { - logger.error("Trying to set value of non-Input element", msg) - return + if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement)) { + logger.error("Trying to set value of non-Input element", msg) + return } const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value doc = this.screen.document @@ -281,7 +281,7 @@ export default class DOMManager extends ListWalker { logger.warn("No iframe doc", msg, node, node.contentDocument); return; } - this.nl[ msg.id ] = doc + this.nl[ msg.id ] = doc.documentElement return; } else if (node instanceof Element) { // shadow DOM try { diff --git a/frontend/app/types/errorInfo.js b/frontend/app/types/errorInfo.js index 364fa8e65..db4c2f3a0 100644 --- a/frontend/app/types/errorInfo.js +++ b/frontend/app/types/errorInfo.js @@ -37,6 +37,7 @@ const ErrorInfo = Record({ chart30: [], tags: [], lastHydratedSession: Session(), + disabled: false, }, { fromJS: ({ stack, lastHydratedSession, ...other }) => ({ ...other, diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index d9b73c224..864613678 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -99,7 +99,7 @@ export default Record({ filters: List(filters) .map(i => { const filter = NewFilter(i).toData(); - if (i.hasOwnProperty('filters')) { + if (Array.isArray(i.filters)) { filter.filters = i.filters.map(f => NewFilter({...f, subFilter: i.type}).toData()); } return filter; diff --git a/scripts/helmcharts/vars.yaml b/scripts/helmcharts/vars.yaml index 5ee4f0508..21e753cdd 100644 --- a/scripts/helmcharts/vars.yaml +++ b/scripts/helmcharts/vars.yaml @@ -42,7 +42,6 @@ kafka: &kafka redis: &redis - # For enterpriseEdition # enabled: false redisHost: "redis-master.db.svc.cluster.local" redisPort: "6379"