diff --git a/api/chalicelib/core/events.py b/api/chalicelib/core/events.py index 87cdd5fa3..6a6d43caa 100644 --- a/api/chalicelib/core/events.py +++ b/api/chalicelib/core/events.py @@ -98,6 +98,22 @@ def get_by_session_id(session_id, project_id, group_clickrage=False, event_type: return rows +def _search_tags(project_id, value, key=None, source=None): + with pg_client.PostgresClient() as cur: + query = f""" + SELECT public.tags.name + '{events.EventType.TAG.ui_type}' AS type + FROM public.tags + WHERE public.tags.project_id = %(project_id)s + ORDER BY SIMILARITY(public.tags.name, %(value)s) DESC + LIMIT 10 + """ + query = cur.mogrify(query, {'project_id': project_id, 'value': value}) + cur.execute(query) + results = helper.list_to_camel_case(cur.fetchall()) + return results + + class EventType: CLICK = Event(ui_type=schemas.EventType.click, table="events.clicks", column="label") INPUT = Event(ui_type=schemas.EventType.input, table="events.inputs", column="label") @@ -106,6 +122,7 @@ class EventType: REQUEST = Event(ui_type=schemas.EventType.request, table="events_common.requests", column="path") GRAPHQL = Event(ui_type=schemas.EventType.graphql, table="events.graphql", column="name") STATEACTION = Event(ui_type=schemas.EventType.state_action, table="events.state_actions", column="name") + TAG = Event(ui_type=schemas.EventType.tag, table="events.tags", column="tag_id") ERROR = Event(ui_type=schemas.EventType.error, table="events.errors", column=None) # column=None because errors are searched by name or message METADATA = Event(ui_type=schemas.FilterType.metadata, table="public.sessions", column=None) @@ -139,6 +156,7 @@ SUPPORTED_TYPES = { EventType.STATEACTION.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.STATEACTION), query=autocomplete.__generic_query( typename=EventType.STATEACTION.ui_type)), + EventType.TAG.ui_type: SupportedFilter(get=_search_tags, query=None), EventType.ERROR.ui_type: SupportedFilter(get=autocomplete.__search_errors, query=None), EventType.METADATA.ui_type: SupportedFilter(get=autocomplete.__search_metadata, diff --git a/api/chalicelib/core/sessions.py b/api/chalicelib/core/sessions.py index 2d41850b2..11278d506 100644 --- a/api/chalicelib/core/sessions.py +++ b/api/chalicelib/core/sessions.py @@ -712,6 +712,11 @@ def search_query_parts(data: schemas.SessionsSearchPayloadSchema, error_status, sh.multi_conditions(f"main.{events.EventType.CLICK_IOS.column} {op} %({e_k})s", event.value, value_key=e_k)) + elif event_type == events.EventType.TAG.ui_type: + event_from = event_from % f"{events.EventType.TAG.table} AS main " + if not is_any: + event_where.append( + sh.multi_conditions(f"main.tag_id = %({e_k})s", event.value, value_key=e_k)) elif event_type == events.EventType.INPUT.ui_type: if platform == "web": event_from = event_from % f"{events.EventType.INPUT.table} AS main " diff --git a/api/chalicelib/core/tags.py b/api/chalicelib/core/tags.py new file mode 100644 index 000000000..17c7be79b --- /dev/null +++ b/api/chalicelib/core/tags.py @@ -0,0 +1,69 @@ +import schemas +from chalicelib.utils import helper +from chalicelib.utils import pg_client + + +def create_tag(project_id: int, data: schemas.TagCreate) -> int: + query = """ + INSERT INTO public.tags (project_id, name, selector, ignore_click_rage, ignore_dead_click) + VALUES (%(project_id)s, %(name)s, %(selector)s, %(ignore_click_rage)s, %(ignore_dead_click)s) + RETURNING tag_id; + """ + + data = { + 'project_id': project_id, + 'name': data.name.strip(), + 'selector': data.selector, + 'ignore_click_rage': data.ignoreClickRage, + 'ignore_dead_click': data.ignoreDeadClick + } + + with pg_client.PostgresClient() as cur: + query = cur.mogrify(query, data) + cur.execute(query) + row = cur.fetchone() + + return row['tag_id'] + + +def list_tags(project_id: int): + query = """ + SELECT tag_id, name, selector, ignore_click_rage, ignore_dead_click + FROM public.tags + WHERE project_id = %(project_id)s + AND deleted_at IS NULL + """ + + with pg_client.PostgresClient() as cur: + query = cur.mogrify(query, {'project_id': project_id}) + cur.execute(query) + rows = cur.fetchall() + + return helper.list_to_camel_case(rows) + + +def update_tag(project_id: int, tag_id: int, data: schemas.TagUpdate): + query = """ + UPDATE public.tags + SET name = %(name)s + WHERE tag_id = %(tag_id)s AND project_id = %(project_id)s + """ + + with pg_client.PostgresClient() as cur: + query = cur.mogrify(query, {'tag_id': tag_id, 'name': data.name, 'project_id': project_id}) + cur.execute(query) + + return True + +def delete_tag(project_id: int, tag_id: int): + query = """ + UPDATE public.tags + SET deleted_at = now() at time zone 'utc' + WHERE tag_id = %(tag_id)s AND project_id = %(project_id)s + """ + + with pg_client.PostgresClient() as cur: + query = cur.mogrify(query, {'tag_id': tag_id, 'project_id': project_id}) + cur.execute(query) + + return True diff --git a/api/routers/core.py b/api/routers/core.py index 61f39b508..3580827f0 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -10,7 +10,7 @@ from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assig log_tool_stackdriver, reset_password, log_tool_cloudwatch, log_tool_sentry, log_tool_sumologic, log_tools, sessions, \ log_tool_newrelic, announcements, log_tool_bugsnag, weekly_report, integration_jira_cloud, integration_github, \ assist, mobile, tenants, boarding, notifications, webhook, users, \ - custom_metrics, saved_search, integrations_global + custom_metrics, saved_search, integrations_global, tags from chalicelib.core.collaboration_msteams import MSTeams from chalicelib.core.collaboration_slack import Slack from or_dependencies import OR_context, OR_role @@ -871,3 +871,28 @@ async def check_recording_status(project_id: int): @public_app.get('/', tags=["health"]) def health_check(): return {} + +# tags + +@app.post('/{projectId}/tags', tags=["tags"]) +def tags_create(projectId: int, data: schemas.TagCreate = Body(), context: schemas.CurrentContext = Depends(OR_context)): + data = tags.create_tag(project_id=projectId, data=data) + return {'data': data} + + +@app.put('/{projectId}/tags/{tagId}', tags=["tags"]) +def tags_update(projectId: int, tagId: int, data: schemas.TagUpdate = Body(), context: schemas.CurrentContext = Depends(OR_context)): + data = tags.update_tag(project_id=projectId, tag_id=tagId, data=data) + return {'data': data} + + +@app.get('/{projectId}/tags', tags=["tags"]) +def tags_list(projectId: int, context: schemas.CurrentContext = Depends(OR_context)): + data = tags.list_tags(project_id=projectId) + return {'data': data} + + +@app.delete('/{projectId}/tags/{tagId}', tags=["tags"]) +def tags_delete(projectId: int, tagId: int, context: schemas.CurrentContext = Depends(OR_context)): + data = tags.delete_tag(projectId, tag_id=tagId) + return {'data': data} diff --git a/api/schemas/schemas.py b/api/schemas/schemas.py index 28cd97286..00a693fb7 100644 --- a/api/schemas/schemas.py +++ b/api/schemas/schemas.py @@ -470,6 +470,7 @@ class EventType(str, Enum): graphql = "graphql" state_action = "stateAction" error = "error" + tag = "tag" click_ios = "tapIos" input_ios = "inputIos" view_ios = "viewIos" @@ -606,7 +607,7 @@ class RequestGraphqlFilterSchema(BaseModel): class SessionSearchEventSchema2(BaseModel): is_event: Literal[True] = True - value: List[str] = Field(...) + value: List[Union[str, int]] = Field(...) type: Union[EventType, PerformanceEventType] = Field(...) operator: Union[SearchEventOperator, ClickEventExtraOperator] = Field(...) source: Optional[List[Union[ErrorSource, int, str]]] = Field(default=None) @@ -1576,3 +1577,15 @@ class ModuleStatus(BaseModel): "offline-recordings", "alerts", "assist-statts", "recommendations", "feature-flags"] = Field(..., description="Possible values: assist, notes, bug-reports, offline-recordings, alerts, assist-statts, recommendations, feature-flags") status: bool = Field(...) + + +class TagUpdate(BaseModel): + name: str = Field(..., min_length=1, max_length=100, pattern='^[a-zA-Z0-9][a-zA-Z0-9_ -]+$') + + +class TagCreate(TagUpdate): + selector: str = Field(..., min_length=1, max_length=255) + ignoreClickRage: bool = Field(default=False) + ignoreDeadClick: bool = Field(default=False) + + diff --git a/ee/api/.gitignore b/ee/api/.gitignore index eabdcc1e4..f7341db45 100644 --- a/ee/api/.gitignore +++ b/ee/api/.gitignore @@ -230,6 +230,7 @@ Pipfile.lock /chalicelib/core/socket_ios.py /chalicelib/core/sourcemaps.py /chalicelib/core/sourcemaps_parser.py +/chalicelib/core/tags.py /chalicelib/saml /chalicelib/utils/__init__.py /chalicelib/utils/args_transformer.py diff --git a/ee/api/chalicelib/core/events.py b/ee/api/chalicelib/core/events.py index 89a159ff7..b74981e03 100644 --- a/ee/api/chalicelib/core/events.py +++ b/ee/api/chalicelib/core/events.py @@ -104,6 +104,22 @@ def get_by_session_id(session_id, project_id, group_clickrage=False, event_type: return rows +def _search_tags(project_id, value, key=None, source=None): + with pg_client.PostgresClient() as cur: + query = f""" + SELECT public.tags.name + '{events.EventType.TAG.ui_type}' AS type + FROM public.tags + WHERE public.tags.project_id = %(project_id)s + ORDER BY SIMILARITY(public.tags.name, %(value)s) DESC + LIMIT 10 + """ + query = cur.mogrify(query, {'project_id': project_id, 'value': value}) + cur.execute(query) + results = helper.list_to_camel_case(cur.fetchall()) + return results + + class EventType: CLICK = Event(ui_type=schemas.EventType.click, table="events.clicks", column="label") INPUT = Event(ui_type=schemas.EventType.input, table="events.inputs", column="label") @@ -112,6 +128,7 @@ class EventType: REQUEST = Event(ui_type=schemas.EventType.request, table="events_common.requests", column="path") GRAPHQL = Event(ui_type=schemas.EventType.graphql, table="events.graphql", column="name") STATEACTION = Event(ui_type=schemas.EventType.state_action, table="events.state_actions", column="name") + TAG = Event(ui_type=schemas.EventType.tag, table="events.tags", column="tag_id") ERROR = Event(ui_type=schemas.EventType.error, table="events.errors", column=None) # column=None because errors are searched by name or message METADATA = Event(ui_type=schemas.FilterType.metadata, table="public.sessions", column=None) @@ -145,6 +162,7 @@ SUPPORTED_TYPES = { EventType.STATEACTION.ui_type: SupportedFilter(get=autocomplete.__generic_autocomplete(EventType.STATEACTION), query=autocomplete.__generic_query( typename=EventType.STATEACTION.ui_type)), + EventType.TAG.ui_type: SupportedFilter(get=_search_tags, query=None), EventType.ERROR.ui_type: SupportedFilter(get=autocomplete.__search_errors, query=None), EventType.METADATA.ui_type: SupportedFilter(get=autocomplete.__search_metadata, diff --git a/ee/api/clean-dev.sh b/ee/api/clean-dev.sh index da52e991c..36c2642e8 100755 --- a/ee/api/clean-dev.sh +++ b/ee/api/clean-dev.sh @@ -52,6 +52,7 @@ rm -rf ./chalicelib/core/socket_ios.py rm -rf ./chalicelib/core/sourcemaps.py rm -rf ./chalicelib/core/sourcemaps_parser.py rm -rf ./chalicelib/core/user_testing.py +rm -rf ./chalicelib/core/tags.py rm -rf ./chalicelib/saml rm -rf ./chalicelib/utils/__init__.py rm -rf ./chalicelib/utils/args_transformer.py @@ -93,4 +94,4 @@ rm -rf ./schemas/overrides.py rm -rf ./schemas/schemas.py rm -rf ./schemas/transformers_validators.py rm -rf ./orpy.py -rm -rf ./chalicelib/core/usability_testing/ \ No newline at end of file +rm -rf ./chalicelib/core/usability_testing/ diff --git a/ee/scripts/schema/db/init_dbs/postgresql/1.17.0/1.17.0.sql b/ee/scripts/schema/db/init_dbs/postgresql/1.17.0/1.17.0.sql index 1a83f1c28..039ce83d1 100644 --- a/ee/scripts/schema/db/init_dbs/postgresql/1.17.0/1.17.0.sql +++ b/ee/scripts/schema/db/init_dbs/postgresql/1.17.0/1.17.0.sql @@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS public.projects_conditions CREATE TABLE IF NOT EXISTS public.tags ( - tag_id bigint NOT NULL PRIMARY KEY, + tag_id serial NOT NULL PRIMARY KEY, name text NOT NULL, project_id integer NOT NULL REFERENCES public.projects (project_id) ON DELETE CASCADE, selector text NOT NULL, @@ -58,7 +58,7 @@ CREATE TABLE IF NOT EXISTS events.tags session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE, timestamp bigint NOT NULL, seq_index integer NOT NULL, - tag_id bigint NOT NULL REFERENCES public.tags (tag_id) ON DELETE CASCADE, + tag_id integer NOT NULL REFERENCES public.tags (tag_id) ON DELETE CASCADE, PRIMARY KEY (session_id, timestamp, seq_index) ); CREATE INDEX IF NOT EXISTS tags_session_id_idx ON events.tags (session_id); diff --git a/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql b/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql index 3d2391569..0f2157908 100644 --- a/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql +++ b/ee/scripts/schema/db/init_dbs/postgresql/init_schema.sql @@ -1156,7 +1156,7 @@ $$ CREATE TABLE public.tags ( - tag_id bigint NOT NULL PRIMARY KEY, + tag_id serial NOT NULL PRIMARY KEY, name text NOT NULL, project_id integer NOT NULL REFERENCES public.projects (project_id) ON DELETE CASCADE, selector text NOT NULL, @@ -1171,7 +1171,7 @@ $$ session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE, timestamp bigint NOT NULL, seq_index integer NOT NULL, - tag_id bigint NOT NULL REFERENCES public.tags (tag_id) ON DELETE CASCADE, + tag_id integer NOT NULL REFERENCES public.tags (tag_id) ON DELETE CASCADE, PRIMARY KEY (session_id, timestamp, seq_index) ); CREATE INDEX tags_session_id_idx ON events.tags (session_id); diff --git a/scripts/schema/db/init_dbs/postgresql/1.17.0/1.17.0.sql b/scripts/schema/db/init_dbs/postgresql/1.17.0/1.17.0.sql index 85637b1e5..e3bdd7350 100644 --- a/scripts/schema/db/init_dbs/postgresql/1.17.0/1.17.0.sql +++ b/scripts/schema/db/init_dbs/postgresql/1.17.0/1.17.0.sql @@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS public.projects_conditions CREATE TABLE IF NOT EXISTS public.tags ( - tag_id bigint NOT NULL PRIMARY KEY, + tag_id serial NOT NULL PRIMARY KEY, name text NOT NULL, project_id integer NOT NULL REFERENCES public.projects (project_id) ON DELETE CASCADE, selector text NOT NULL, @@ -58,7 +58,7 @@ CREATE TABLE IF NOT EXISTS events.tags session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE, timestamp bigint NOT NULL, seq_index integer NOT NULL, - tag_id bigint NOT NULL REFERENCES public.tags (tag_id) ON DELETE CASCADE, + tag_id integer NOT NULL REFERENCES public.tags (tag_id) ON DELETE CASCADE, PRIMARY KEY (session_id, timestamp, seq_index) ); CREATE INDEX IF NOT EXISTS tags_session_id_idx ON events.tags (session_id); diff --git a/scripts/schema/db/init_dbs/postgresql/init_schema.sql b/scripts/schema/db/init_dbs/postgresql/init_schema.sql index 496104191..c8a4f3d3f 100644 --- a/scripts/schema/db/init_dbs/postgresql/init_schema.sql +++ b/scripts/schema/db/init_dbs/postgresql/init_schema.sql @@ -1117,7 +1117,7 @@ $$ CREATE TABLE public.tags ( - tag_id bigint NOT NULL PRIMARY KEY, + tag_id serial NOT NULL PRIMARY KEY, name text NOT NULL, project_id integer NOT NULL REFERENCES public.projects (project_id) ON DELETE CASCADE, selector text NOT NULL, @@ -1132,7 +1132,7 @@ $$ session_id bigint NOT NULL REFERENCES public.sessions (session_id) ON DELETE CASCADE, timestamp bigint NOT NULL, seq_index integer NOT NULL, - tag_id bigint NOT NULL REFERENCES public.tags (tag_id) ON DELETE CASCADE, + tag_id integer NOT NULL REFERENCES public.tags (tag_id) ON DELETE CASCADE, PRIMARY KEY (session_id, timestamp, seq_index) ); CREATE INDEX tags_session_id_idx ON events.tags (session_id);