-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(replays): Quick poc of replays on the events endpoint #104083
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| from sentry.search.eap import constants | ||
| from sentry.search.eap.columns import ( | ||
| ResolvedAttribute, | ||
| VirtualColumnDefinition, | ||
| project_context_constructor, | ||
| project_term_resolver, | ||
| ) | ||
| from sentry.search.eap.common_columns import COMMON_COLUMNS | ||
| from sentry.utils.validators import is_event_id_or_list | ||
|
|
||
| # TODO(wmak): Most of this is copy paste from the other attribute definitions, just trying to get a quick POC | ||
| REPLAY_ATTRIBUTE_DEFINITIONS = { | ||
| column.public_alias: column | ||
| for column in COMMON_COLUMNS | ||
| + [ | ||
| ResolvedAttribute( | ||
| public_alias="id", | ||
| internal_name="sentry.item_id", | ||
| search_type="string", | ||
| validator=is_event_id_or_list, | ||
| ), | ||
| ] | ||
| } | ||
|
|
||
|
|
||
| REPLAY_VIRTUAL_CONTEXTS = { | ||
| key: VirtualColumnDefinition( | ||
| constructor=project_context_constructor(key), | ||
| term_resolver=project_term_resolver, | ||
| filter_column="project.id", | ||
| ) | ||
| for key in constants.PROJECT_FIELDS | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| from sentry_protos.snuba.v1.request_common_pb2 import TraceItemType | ||
|
|
||
| from sentry.search.eap.columns import ColumnDefinitions | ||
| from sentry.search.eap.replays.attributes import ( | ||
| REPLAY_ATTRIBUTE_DEFINITIONS, | ||
| REPLAY_VIRTUAL_CONTEXTS, | ||
| ) | ||
|
|
||
| REPLAY_DEFINITIONS = ColumnDefinitions( | ||
| aggregates={}, | ||
| conditional_aggregates={}, | ||
| formulas={}, | ||
| columns=REPLAY_ATTRIBUTE_DEFINITIONS, | ||
| contexts=REPLAY_VIRTUAL_CONTEXTS, | ||
| trace_item_type=TraceItemType.TRACE_ITEM_TYPE_REPLAY, | ||
| filter_aliases={}, | ||
| column_to_alias=None, | ||
| alias_to_column=None, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import logging | ||
|
|
||
| import sentry_sdk | ||
| from sentry_protos.snuba.v1.request_common_pb2 import PageToken | ||
|
|
||
| from sentry.search.eap.replays.definitions import REPLAY_DEFINITIONS | ||
| from sentry.search.eap.resolver import SearchResolver | ||
| from sentry.search.eap.types import AdditionalQueries, EAPResponse, SearchResolverConfig | ||
| from sentry.search.events.types import SAMPLING_MODES, SnubaParams | ||
| from sentry.snuba import rpc_dataset_common | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class Replays(rpc_dataset_common.RPCBase): | ||
| DEFINITIONS = REPLAY_DEFINITIONS | ||
|
|
||
| @classmethod | ||
| @sentry_sdk.trace | ||
| def run_table_query( | ||
| cls, | ||
| *, | ||
| params: SnubaParams, | ||
| query_string: str, | ||
| selected_columns: list[str], | ||
| orderby: list[str] | None, | ||
| offset: int, | ||
| limit: int, | ||
| referrer: str, | ||
| config: SearchResolverConfig, | ||
| sampling_mode: SAMPLING_MODES | None = None, | ||
| equations: list[str] | None = None, | ||
| search_resolver: SearchResolver | None = None, | ||
| page_token: PageToken | None = None, | ||
| additional_queries: AdditionalQueries | None = None, | ||
| ) -> EAPResponse: | ||
| return cls._run_table_query( | ||
| rpc_dataset_common.TableQuery( | ||
| query_string=query_string, | ||
| selected_columns=selected_columns, | ||
| equations=equations, | ||
| orderby=orderby, | ||
| offset=offset, | ||
| limit=limit, | ||
| referrer=referrer, | ||
| sampling_mode=sampling_mode, | ||
| page_token=page_token, | ||
| resolver=search_resolver or cls.get_resolver(params, config), | ||
| additional_queries=additional_queries, | ||
| ), | ||
| params.debug, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2289,11 +2289,67 @@ def setUp(self): | |
| super().setUp() | ||
| assert requests.post(settings.SENTRY_SNUBA + "/tests/replays/drop").status_code == 200 | ||
|
|
||
| def store_replays(self, replay): | ||
| response = requests.post( | ||
| settings.SENTRY_SNUBA + "/tests/entities/replays/insert", json=[replay] | ||
| def create_replay( | ||
| self, | ||
| timestamp: datetime, | ||
| project: Project | None = None, | ||
| replay_id: str | None = None, | ||
| trace_id: str | None = None, | ||
| ) -> TraceItem: | ||
| if project is None: | ||
| project = self.project | ||
| if replay_id is None: | ||
| replay_id = uuid4().hex | ||
| if trace_id is None: | ||
| trace_id = uuid4().hex | ||
| rpc_timestamp = Timestamp() | ||
| rpc_timestamp.FromDatetime(timestamp) | ||
| return TraceItem( | ||
| organization_id=project.organization.id, | ||
| project_id=project.id, | ||
| item_type=TraceItemType.TRACE_ITEM_TYPE_REPLAY, | ||
| timestamp=rpc_timestamp, | ||
| trace_id=trace_id, | ||
| item_id=int(replay_id, 16).to_bytes( | ||
| 16, | ||
| byteorder="little", | ||
| signed=False, | ||
| ), | ||
| received=rpc_timestamp, | ||
| retention_days=90, | ||
| attributes={}, | ||
| client_sample_rate=1.0, | ||
| server_sample_rate=1.0, | ||
| ) | ||
| assert response.status_code == 200 | ||
|
|
||
| def store_replay(self, replay): | ||
| assert ( | ||
| requests.post( | ||
| settings.SENTRY_SNUBA + EAP_ITEMS_INSERT_ENDPOINT, | ||
| files={"item_0": replay.SerializeToString()}, | ||
| ).status_code | ||
| == 200 | ||
| ) | ||
|
|
||
| def store_replays(self, replays, is_eap=False): | ||
| """Trying to deprecate the single replay store functionality here with `store_replay` so when is_eap is True | ||
| this function will attempt to store multiple replays""" | ||
| if is_eap: | ||
| assert ( | ||
| requests.post( | ||
| settings.SENTRY_SNUBA + EAP_ITEMS_INSERT_ENDPOINT, | ||
| files={ | ||
| f"item_{index}": replay.SerializeToString() | ||
| for index, replay in enumerate(replays) | ||
| }, | ||
| ).status_code | ||
| == 200 | ||
| ) | ||
| else: | ||
| response = requests.post( | ||
| settings.SENTRY_SNUBA + "/tests/entities/replays/insert", json=[replays] | ||
| ) | ||
| assert response.status_code == 200 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Non-EAP path wraps replays list creating nested arrayThe |
||
|
|
||
| def mock_event_links(self, timestamp, project_id, level, replay_id, event_id): | ||
| event = self.store_event( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| from uuid import uuid4 | ||
|
|
||
| import pytest | ||
|
|
||
| from sentry.testutils.cases import ReplaysSnubaTestCase | ||
| from tests.snuba.api.endpoints.test_organization_events import OrganizationEventsEndpointTestBase | ||
|
|
||
|
|
||
| class OrganizationEventsReplaysEndpointTest( | ||
| OrganizationEventsEndpointTestBase, ReplaysSnubaTestCase | ||
| ): | ||
| dataset = "replays" | ||
|
|
||
| def setUp(self) -> None: | ||
| super().setUp() | ||
| self.features = { | ||
| "organizations:session-replay": True, | ||
| } | ||
|
|
||
| @pytest.mark.querybuilder | ||
| def test_simple(self) -> None: | ||
| replay_ids = [uuid4().hex, uuid4().hex] | ||
| self.store_replays( | ||
| [ | ||
| self.create_replay(self.ten_mins_ago, replay_id=replay_id) | ||
| for replay_id in replay_ids | ||
| ], | ||
| is_eap=True, | ||
| ) | ||
| response = self.do_request( | ||
| { | ||
| "field": ["id"], | ||
| "query": "", | ||
| "orderby": "-id", | ||
| "project": self.project.id, | ||
| "dataset": self.dataset, | ||
| } | ||
| ) | ||
| assert response.status_code == 200, response.content | ||
| data = response.data["data"] | ||
| assert len(data) == 2 | ||
| for replay_id in replay_ids: | ||
| assert replay_id in [row["id"] for row in data] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The
Replaysdataset, when queried for timeseries data, will raise an unhandledNotImplementedError().Severity: CRITICAL | Confidence: High
🔍 Detailed Analysis
The
Replaysclass, now exposed viaRPC_DATASETSfor the events endpoint, lacks implementations forrun_timeseries_query(),run_trace_query(), andrun_stats_query(). If a user queriesorganization_events_stats.pywithdataset=replaysand timeseries parameters, the system will attempt to callrun_timeseries_query()on theReplaysobject, which raises an unhandledNotImplementedError(), resulting in a 500 error.💡 Suggested Fix
Implement
run_timeseries_query(),run_trace_query(), andrun_stats_query()methods in theReplaysclass insrc/sentry/snuba/replays.pyto handle timeseries, trace, and stats queries, or explicitly prevent these query types for theReplaysdataset.🤖 Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID:
3917707