Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion src/sentry/replays/endpoints/organization_replay_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
from rest_framework.response import Response
from snuba_sdk import Column, Condition, Entity, Function, Granularity, Op, Query
from snuba_sdk import (
Column,
Condition,
Direction,
Entity,
Function,
Granularity,
Op,
OrderBy,
Query,
)

from sentry import features
from sentry.api.api_owners import ApiOwner
Expand All @@ -24,6 +34,68 @@
from sentry.replays.validators import ReplayValidator


def _query_replay_urls_eap(
replay_id: str,
project_ids: list[int],
start: datetime,
end: datetime,
organization_id: int,
) -> list[str]:
"""Query URLs for a replay from EAP breadcrumb events."""
replay_id_no_dashes = replay_id.replace("-", "")

first_seen_agg = Function("min", parameters=[Column("sentry.timestamp")], alias="first_seen")

select = [
Column("to"),
first_seen_agg,
]

snuba_query = Query(
match=Entity("replays"),
select=select,
where=[
Condition(Column("replay_id"), Op.EQ, replay_id_no_dashes),
Condition(Column("category"), Op.EQ, "navigation"),
],
groupby=[Column("to")],
orderby=[OrderBy(first_seen_agg, Direction.ASC)],
)

settings = Settings(
attribute_types={
"replay_id": str,
"category": str,
"sentry.timestamp": float,
"to": str,
},
default_limit=1000,
default_offset=0,
)

request_meta = RequestMeta(
cogs_category="replays",
debug=False,
start_datetime=start,
end_datetime=end,
organization_id=organization_id,
project_ids=project_ids,
referrer="replays.query.urls",
request_id=str(uuid.uuid4().hex),
trace_item_type="replay",
)

result = eap_read.query(snuba_query, settings, request_meta, [])

urls: list[str] = []
for row in result.get("data", []):
url = row.get("to")
if url and isinstance(url, str):
urls.append(url)

return urls


def _normalize_eap_response(data: list[dict]) -> list[dict]:
"""Normalize EAP response data for frontend compatibility.

Expand Down Expand Up @@ -202,6 +274,16 @@ def get(self, request: Request, organization: Organization, replay_id: str) -> R
organization_id=organization.id,
request_user_id=request.user.id,
)["data"]

if snuba_response:
urls = _query_replay_urls_eap(
replay_id=replay_id,
project_ids=project_ids,
start=filter_params["start"],
end=filter_params["end"],
organization_id=organization.id,
)
snuba_response[0]["urls_sorted"] = urls
else:
snuba_response = query_replay_instance(
project_id=project_ids,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from sentry.replays.endpoints.organization_replay_details import (
_normalize_eap_response,
_query_replay_urls_eap,
query_replay_instance_eap,
)
from sentry.testutils.cases import ReplayBreadcrumbType, ReplayEAPTestCase, TestCase
Expand Down Expand Up @@ -47,6 +48,33 @@ def test_eap_replay_query(self) -> None:
breadcrumb_type=ReplayBreadcrumbType.CLICK,
timestamp=now,
),
self.create_eap_replay_breadcrumb(
project=self.project,
replay_id=replay_id1,
segment_id=0,
breadcrumb_type=ReplayBreadcrumbType.CLICK,
timestamp=now - datetime.timedelta(seconds=30),
category="navigation",
to="https://example.com/page1",
),
self.create_eap_replay_breadcrumb(
project=self.project,
replay_id=replay_id1,
segment_id=0,
breadcrumb_type=ReplayBreadcrumbType.CLICK,
timestamp=now - datetime.timedelta(seconds=20),
category="navigation",
to="https://example.com/page2",
),
self.create_eap_replay_breadcrumb(
project=self.project,
replay_id=replay_id1,
segment_id=0,
breadcrumb_type=ReplayBreadcrumbType.CLICK,
timestamp=now - datetime.timedelta(seconds=10),
category="navigation",
to="https://example.com/page3",
),
]

replay2_breadcrumbs = [
Expand Down Expand Up @@ -138,6 +166,30 @@ def test_eap_replay_query(self) -> None:
assert replay2_data["count_dead_clicks"] == 3, "1 DEAD_CLICK + 2 RAGE_CLICK = 3 dead"
assert replay2_data["count_rage_clicks"] == 2, "2 RAGE_CLICK = 2 rage"

# Test URL query for replay1
urls = _query_replay_urls_eap(
replay_id=replay_id1,
project_ids=project_ids,
start=start,
end=end,
organization_id=organization_id,
)
assert len(urls) == 3, f"Expected 3 URLs, got {len(urls)}"
assert urls == [
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
], f"URLs should be sorted by timestamp ascending, got {urls}"

urls2 = _query_replay_urls_eap(
replay_id=replay_id2,
project_ids=project_ids,
start=start,
end=end,
organization_id=organization_id,
)
assert len(urls2) == 0, f"Expected 0 URLs for replay2, got {len(urls2)}"

def test_normalize_eap_response(self) -> None:
"""Test that EAP response data is correctly normalized.

Expand Down
Loading