test_organization_replay_index.py 66 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679
  1. import datetime
  2. import uuid
  3. from unittest import mock
  4. import pytest
  5. from django.urls import reverse
  6. from sentry.replays.testutils import (
  7. assert_expected_response,
  8. mock_expected_response,
  9. mock_replay,
  10. mock_replay_click,
  11. )
  12. from sentry.testutils.cases import APITestCase, ReplaysSnubaTestCase
  13. from sentry.testutils.helpers.features import apply_feature_flag_on_cls
  14. from sentry.testutils.silo import region_silo_test
  15. from sentry.utils.cursors import Cursor
  16. from sentry.utils.snuba import QueryMemoryLimitExceeded
  17. REPLAYS_FEATURES = {"organizations:session-replay": True}
  18. @region_silo_test(stable=True)
  19. @apply_feature_flag_on_cls("organizations:global-views")
  20. class OrganizationReplayIndexTest(APITestCase, ReplaysSnubaTestCase):
  21. endpoint = "sentry-api-0-organization-replay-index"
  22. def setUp(self):
  23. super().setUp()
  24. self.login_as(user=self.user)
  25. self.url = reverse(self.endpoint, args=(self.organization.slug,))
  26. def test_feature_flag_disabled(self):
  27. """Test replays can be disabled."""
  28. response = self.client.get(self.url)
  29. assert response.status_code == 404
  30. def test_no_projects(self):
  31. """Test replays must be used with a project(s)."""
  32. with self.feature(REPLAYS_FEATURES):
  33. response = self.client.get(self.url)
  34. assert response.status_code == 200
  35. response_data = response.json()
  36. assert "data" in response_data
  37. assert response_data["data"] == []
  38. def test_get_replays(self):
  39. """Test replays conform to the interchange format."""
  40. project = self.create_project(teams=[self.team])
  41. replay1_id = uuid.uuid4().hex
  42. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  43. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  44. self.store_replays(
  45. mock_replay(
  46. seq1_timestamp,
  47. project.id,
  48. replay1_id,
  49. # NOTE: This is commented out due to a bug in CI. This will not affect
  50. # production use and have been verfied as working as of 08/10/2022.
  51. #
  52. # error_ids=[uuid.uuid4().hex, replay1_id], # duplicate error-id
  53. urls=[
  54. "http://localhost:3000/",
  55. "http://localhost:3000/login",
  56. ], # duplicate urls are okay,
  57. tags={"test": "hello", "other": "hello"},
  58. release="test",
  59. )
  60. )
  61. self.store_replays(
  62. mock_replay(
  63. seq2_timestamp,
  64. project.id,
  65. replay1_id,
  66. # error_ids=[uuid.uuid4().hex, replay1_id], # duplicate error-id
  67. urls=["http://localhost:3000/"], # duplicate urls are okay
  68. tags={"test": "world", "other": "hello"},
  69. error_ids=[],
  70. release="",
  71. )
  72. )
  73. self.store_replays(
  74. mock_replay_click(
  75. seq2_timestamp,
  76. project.id,
  77. replay1_id,
  78. node_id=1,
  79. tag="div",
  80. id="myid",
  81. class_=["class1", "class2"],
  82. role="button",
  83. testid="1",
  84. alt="Alt",
  85. aria_label="AriaLabel",
  86. title="MyTitle",
  87. is_dead=1,
  88. is_rage=1,
  89. text="Hello",
  90. release=None,
  91. )
  92. )
  93. with self.feature(REPLAYS_FEATURES):
  94. response = self.client.get(self.url)
  95. assert response.status_code == 200
  96. response_data = response.json()
  97. assert "data" in response_data
  98. assert len(response_data["data"]) == 1
  99. # Assert the response body matches what was expected.
  100. expected_response = mock_expected_response(
  101. project.id,
  102. replay1_id,
  103. seq1_timestamp,
  104. seq2_timestamp,
  105. urls=[
  106. "http://localhost:3000/",
  107. "http://localhost:3000/login",
  108. "http://localhost:3000/",
  109. ],
  110. count_segments=2,
  111. # count_errors=3,
  112. count_errors=1,
  113. tags={"test": ["hello", "world"], "other": ["hello"]},
  114. activity=4,
  115. count_dead_clicks=1,
  116. count_rage_clicks=1,
  117. releases=["test"],
  118. clicks=[
  119. {
  120. "click.alt": "Alt",
  121. "click.classes": ["class1", "class2"],
  122. "click.id": "myid",
  123. "click.role": "button",
  124. "click.tag": "div",
  125. "click.testid": "1",
  126. "click.text": "Hello",
  127. "click.title": "MyTitle",
  128. "click.label": "AriaLabel",
  129. }
  130. ],
  131. )
  132. assert_expected_response(response_data["data"][0], expected_response)
  133. def test_get_replays_browse_screen_fields(self):
  134. """Test replay response with fields requested in production."""
  135. project = self.create_project(teams=[self.team])
  136. replay1_id = uuid.uuid4().hex
  137. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  138. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  139. self.store_replays(
  140. mock_replay(
  141. seq1_timestamp,
  142. project.id,
  143. replay1_id,
  144. urls=[
  145. "http://localhost:3000/",
  146. "http://localhost:3000/login",
  147. ],
  148. tags={"test": "hello", "other": "hello"},
  149. )
  150. )
  151. self.store_replays(
  152. mock_replay(
  153. seq2_timestamp,
  154. project.id,
  155. replay1_id,
  156. urls=["http://localhost:3000/"],
  157. tags={"test": "world", "other": "hello"},
  158. )
  159. )
  160. with self.feature(REPLAYS_FEATURES):
  161. response = self.client.get(
  162. self.url
  163. + "?field=activity&field=count_errors&field=duration&field=finished_at&field=id"
  164. "&field=project_id&field=started_at&field=urls&field=user"
  165. )
  166. assert response.status_code == 200
  167. response_data = response.json()
  168. assert "data" in response_data
  169. assert len(response_data["data"]) == 1
  170. assert len(response_data["data"][0]) == 9
  171. assert "activity" in response_data["data"][0]
  172. assert "count_errors" in response_data["data"][0]
  173. assert "duration" in response_data["data"][0]
  174. assert "finished_at" in response_data["data"][0]
  175. assert "id" in response_data["data"][0]
  176. assert "project_id" in response_data["data"][0]
  177. assert "started_at" in response_data["data"][0]
  178. assert "urls" in response_data["data"][0]
  179. assert "user" in response_data["data"][0]
  180. assert len(response_data["data"][0]["user"]) == 5
  181. assert "id" in response_data["data"][0]["user"]
  182. assert "username" in response_data["data"][0]["user"]
  183. assert "email" in response_data["data"][0]["user"]
  184. assert "ip" in response_data["data"][0]["user"]
  185. assert "display_name" in response_data["data"][0]["user"]
  186. def test_get_replays_tags_field(self):
  187. """Test replay response with fields requested in production."""
  188. project = self.create_project(teams=[self.team])
  189. replay1_id = uuid.uuid4().hex
  190. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  191. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  192. self.store_replays(
  193. mock_replay(
  194. seq1_timestamp,
  195. project.id,
  196. replay1_id,
  197. urls=[
  198. "http://localhost:3000/",
  199. "http://localhost:3000/login",
  200. ],
  201. tags={"test": "hello", "other": "hello"},
  202. )
  203. )
  204. self.store_replays(
  205. mock_replay(
  206. seq2_timestamp,
  207. project.id,
  208. replay1_id,
  209. urls=["http://localhost:3000/"],
  210. tags={"test": "world", "other": "hello"},
  211. )
  212. )
  213. with self.feature(REPLAYS_FEATURES):
  214. response = self.client.get(self.url + "?field=tags")
  215. assert response.status_code == 200
  216. response_data = response.json()
  217. assert "data" in response_data
  218. assert len(response_data["data"]) == 1
  219. assert len(response_data["data"][0]) == 1
  220. assert "tags" in response_data["data"][0]
  221. assert sorted(response_data["data"][0]["tags"]["test"]) == ["hello", "world"]
  222. assert response_data["data"][0]["tags"]["other"] == ["hello"]
  223. def test_get_replays_minimum_field_set(self):
  224. """Test replay response with fields requested in production."""
  225. project = self.create_project(teams=[self.team])
  226. replay1_id = uuid.uuid4().hex
  227. replay2_id = uuid.uuid4().hex
  228. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  229. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  230. self.store_replays(
  231. mock_replay(
  232. seq2_timestamp,
  233. project.id,
  234. replay1_id,
  235. urls=[
  236. "http://localhost:3000/",
  237. "http://localhost:3000/login",
  238. ],
  239. tags={"test": "hello", "other": "hello"},
  240. user_id=123,
  241. replay_start_timestamp=int(seq1_timestamp.timestamp()),
  242. )
  243. )
  244. self.store_replays(
  245. mock_replay(
  246. seq2_timestamp,
  247. project.id,
  248. replay2_id,
  249. urls=["http://localhost:3000/"],
  250. tags={"test": "world", "other": "hello"},
  251. replay_start_timestamp=int(seq1_timestamp.timestamp()),
  252. )
  253. )
  254. with self.feature(REPLAYS_FEATURES):
  255. response = self.client.get(
  256. self.url + "?field=id&sort=count_errors&query=test:hello OR user_id:123"
  257. )
  258. assert response.status_code == 200
  259. response_data = response.json()
  260. assert "data" in response_data
  261. assert len(response_data["data"]) == 1
  262. assert len(response_data["data"][0]) == 1
  263. assert "id" in response_data["data"][0]
  264. def test_get_replays_filter_environment(self):
  265. """Test returned replays can not partially fall outside of range."""
  266. project = self.create_project(teams=[self.team])
  267. self.create_environment(name="development", project=self.project)
  268. self.create_environment(name="production", project=self.project)
  269. replay1_id = uuid.uuid4().hex
  270. replay2_id = uuid.uuid4().hex
  271. timestamp0 = datetime.datetime.now() - datetime.timedelta(seconds=20)
  272. timestamp1 = datetime.datetime.now() - datetime.timedelta(seconds=10)
  273. self.store_replays(
  274. mock_replay(timestamp0, project.id, replay1_id, environment="development")
  275. )
  276. self.store_replays(
  277. mock_replay(timestamp1, project.id, replay1_id, environment="development")
  278. )
  279. self.store_replays(
  280. mock_replay(timestamp0, project.id, replay2_id, environment="production")
  281. )
  282. self.store_replays(
  283. mock_replay(timestamp1, project.id, replay2_id, environment="production")
  284. )
  285. with self.feature(REPLAYS_FEATURES):
  286. response = self.client.get(self.url + "?environment=development")
  287. assert response.status_code == 200
  288. response_data = response.json()
  289. assert "data" in response_data
  290. assert response_data["data"][0]["id"] == replay1_id
  291. response = self.client.get(self.url + "?environment=production")
  292. assert response.status_code == 200
  293. response_data = response.json()
  294. assert "data" in response_data
  295. assert response_data["data"][0]["id"] == replay2_id
  296. def test_get_replays_started_at_sorted(self):
  297. project = self.create_project(teams=[self.team])
  298. replay1_id = uuid.uuid4().hex
  299. replay2_id = uuid.uuid4().hex
  300. replay1_timestamp0 = datetime.datetime.now() - datetime.timedelta(seconds=15)
  301. replay1_timestamp1 = datetime.datetime.now() - datetime.timedelta(seconds=5)
  302. replay2_timestamp0 = datetime.datetime.now() - datetime.timedelta(seconds=10)
  303. replay2_timestamp1 = datetime.datetime.now() - datetime.timedelta(seconds=2)
  304. self.store_replays(mock_replay(replay1_timestamp0, project.id, replay1_id))
  305. self.store_replays(mock_replay(replay1_timestamp1, project.id, replay1_id))
  306. self.store_replays(mock_replay(replay2_timestamp0, project.id, replay2_id))
  307. self.store_replays(mock_replay(replay2_timestamp1, project.id, replay2_id))
  308. with self.feature(REPLAYS_FEATURES):
  309. # Latest first.
  310. response = self.client.get(self.url + "?sort=-started_at")
  311. response_data = response.json()
  312. assert response_data["data"][0]["id"] == replay2_id
  313. assert response_data["data"][1]["id"] == replay1_id
  314. # Earlist first.
  315. response = self.client.get(self.url + "?sort=started_at")
  316. response_data = response.json()
  317. assert response_data["data"][0]["id"] == replay1_id
  318. assert response_data["data"][1]["id"] == replay2_id
  319. def test_get_replays_finished_at_sorted(self):
  320. project = self.create_project(teams=[self.team])
  321. replay1_id = uuid.uuid4().hex
  322. replay2_id = uuid.uuid4().hex
  323. replay1_timestamp0 = datetime.datetime.now() - datetime.timedelta(seconds=15)
  324. replay1_timestamp1 = datetime.datetime.now() - datetime.timedelta(seconds=5)
  325. replay2_timestamp0 = datetime.datetime.now() - datetime.timedelta(seconds=10)
  326. replay2_timestamp1 = datetime.datetime.now() - datetime.timedelta(seconds=2)
  327. self.store_replays(mock_replay(replay1_timestamp0, project.id, replay1_id))
  328. self.store_replays(mock_replay(replay1_timestamp1, project.id, replay1_id))
  329. self.store_replays(mock_replay(replay2_timestamp0, project.id, replay2_id))
  330. self.store_replays(mock_replay(replay2_timestamp1, project.id, replay2_id))
  331. with self.feature(REPLAYS_FEATURES):
  332. # Latest first.
  333. response = self.client.get(self.url + "?sort=-finished_at")
  334. response_data = response.json()
  335. assert response_data["data"][0]["id"] == replay2_id
  336. assert response_data["data"][1]["id"] == replay1_id
  337. # Earlist first.
  338. response = self.client.get(self.url + "?sort=finished_at")
  339. response_data = response.json()
  340. assert response_data["data"][0]["id"] == replay1_id
  341. assert response_data["data"][1]["id"] == replay2_id
  342. def test_get_replays_duration_sorted(self):
  343. """Test replays can be sorted by duration."""
  344. project = self.create_project(teams=[self.team])
  345. replay1_id = uuid.uuid4().hex
  346. replay2_id = uuid.uuid4().hex
  347. replay1_timestamp0 = datetime.datetime.now() - datetime.timedelta(seconds=15)
  348. replay1_timestamp1 = datetime.datetime.now() - datetime.timedelta(seconds=10)
  349. replay2_timestamp0 = datetime.datetime.now() - datetime.timedelta(seconds=9)
  350. replay2_timestamp1 = datetime.datetime.now() - datetime.timedelta(seconds=2)
  351. self.store_replays(mock_replay(replay1_timestamp0, project.id, replay1_id))
  352. self.store_replays(mock_replay(replay1_timestamp1, project.id, replay1_id))
  353. self.store_replays(mock_replay(replay2_timestamp0, project.id, replay2_id))
  354. self.store_replays(mock_replay(replay2_timestamp1, project.id, replay2_id))
  355. with self.feature(REPLAYS_FEATURES):
  356. # Smallest duration first.
  357. response = self.client.get(self.url + "?sort=duration")
  358. assert response.status_code == 200, response
  359. response_data = response.json()
  360. assert response_data["data"][0]["id"] == replay1_id
  361. assert response_data["data"][1]["id"] == replay2_id
  362. # Largest duration first.
  363. response = self.client.get(self.url + "?sort=-duration")
  364. response_data = response.json()
  365. assert response_data["data"][0]["id"] == replay2_id
  366. assert response_data["data"][1]["id"] == replay1_id
  367. def test_get_replays_pagination(self):
  368. """Test replays can be paginated."""
  369. project = self.create_project(teams=[self.team])
  370. replay1_id = uuid.uuid4().hex
  371. replay2_id = uuid.uuid4().hex
  372. replay1_timestamp0 = datetime.datetime.now() - datetime.timedelta(seconds=15)
  373. replay1_timestamp1 = datetime.datetime.now() - datetime.timedelta(seconds=5)
  374. replay2_timestamp0 = datetime.datetime.now() - datetime.timedelta(seconds=10)
  375. replay2_timestamp1 = datetime.datetime.now() - datetime.timedelta(seconds=2)
  376. self.store_replays(mock_replay(replay1_timestamp0, project.id, replay1_id, segment_id=0))
  377. self.store_replays(mock_replay(replay1_timestamp1, project.id, replay1_id, segment_id=1))
  378. self.store_replays(mock_replay(replay2_timestamp0, project.id, replay2_id, segment_id=0))
  379. self.store_replays(mock_replay(replay2_timestamp1, project.id, replay2_id, segment_id=1))
  380. with self.feature(REPLAYS_FEATURES):
  381. # First page.
  382. response = self.get_success_response(
  383. self.organization.slug,
  384. cursor=Cursor(0, 0),
  385. per_page=1,
  386. )
  387. response_data = response.json()
  388. assert "data" in response_data
  389. assert len(response_data["data"]) == 1
  390. assert response_data["data"][0]["id"] == replay2_id
  391. # Next page.
  392. response = self.get_success_response(
  393. self.organization.slug,
  394. cursor=Cursor(0, 1),
  395. per_page=1,
  396. )
  397. response_data = response.json()
  398. assert "data" in response_data
  399. assert len(response_data["data"]) == 1
  400. assert response_data["data"][0]["id"] == replay1_id
  401. # Beyond pages.
  402. response = self.get_success_response(
  403. self.organization.slug,
  404. cursor=Cursor(0, 2),
  405. per_page=1,
  406. )
  407. response_data = response.json()
  408. assert "data" in response_data
  409. assert len(response_data["data"]) == 0
  410. def test_get_replays_user_filters(self):
  411. """Test replays conform to the interchange format."""
  412. project = self.create_project(teams=[self.team])
  413. replay1_id = uuid.uuid4().hex
  414. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  415. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  416. self.store_replays(
  417. mock_replay(
  418. seq1_timestamp,
  419. project.id,
  420. replay1_id,
  421. platform="javascript",
  422. dist="abc123",
  423. user_id="123",
  424. user_email="username@example.com",
  425. user_name="username123",
  426. user_ip_address="127.0.0.1",
  427. sdk_name="sentry.javascript.react",
  428. sdk_version="6.18.10",
  429. os_name="macOS",
  430. os_version="15",
  431. browser_name="Firefox",
  432. browser_version="99",
  433. device_name="Macbook",
  434. device_brand="Apple",
  435. device_family="Macintosh",
  436. device_model="10",
  437. tags={"a": "m", "b": "q", "c": "test"},
  438. urls=["example.com"],
  439. segment_id=0,
  440. )
  441. )
  442. self.store_replays(
  443. mock_replay(
  444. seq2_timestamp,
  445. project.id,
  446. replay1_id,
  447. user_id=None,
  448. user_name=None,
  449. user_email=None,
  450. ipv4=None,
  451. os_name=None,
  452. os_version=None,
  453. browser_name=None,
  454. browser_version=None,
  455. device_name=None,
  456. device_brand=None,
  457. device_family=None,
  458. device_model=None,
  459. tags={"a": "n", "b": "o"},
  460. error_ids=[],
  461. segment_id=1,
  462. )
  463. )
  464. with self.feature(REPLAYS_FEATURES):
  465. # Run all the queries individually to determine compliance.
  466. queries = [
  467. "replay_type:session",
  468. "error_ids:a3a62ef6ac86415b83c2416fc2f76db1",
  469. "error_id:a3a62ef6ac86415b83c2416fc2f76db1",
  470. "trace_ids:4491657243ba4dbebd2f6bd62b733080",
  471. "trace_id:4491657243ba4dbebd2f6bd62b733080",
  472. "trace:4491657243ba4dbebd2f6bd62b733080",
  473. "count_urls:1",
  474. "count_dead_clicks:0",
  475. "count_rage_clicks:0",
  476. "platform:javascript",
  477. "releases:version@1.3",
  478. "releases:[a,version@1.3]",
  479. "release:version@1.3",
  480. "release:[a,version@1.3]",
  481. "duration:17",
  482. "!duration:16",
  483. "duration:>16",
  484. "duration:<18",
  485. "duration:>=17",
  486. "duration:<=17",
  487. "duration:[16,17]",
  488. "!duration:[16,18]",
  489. "user.id:123",
  490. "user:username123",
  491. "user.username:username123",
  492. "user.email:username@example.com",
  493. "user.email:*@example.com",
  494. "user.ip:127.0.0.1",
  495. "sdk.name:sentry.javascript.react",
  496. "os.name:macOS",
  497. "os.version:15",
  498. "browser.name:Firefox",
  499. "browser.version:99",
  500. "dist:abc123",
  501. "releases:*3",
  502. "!releases:*4",
  503. "release:*3",
  504. "!release:*4",
  505. "count_segments:>=2",
  506. "device.name:Macbook",
  507. "device.brand:Apple",
  508. "device.family:Macintosh",
  509. "device.model:10",
  510. # Contains operator.
  511. f"id:[{replay1_id},{uuid.uuid4().hex},{uuid.uuid4().hex}]",
  512. f"!id:[{uuid.uuid4().hex}]",
  513. # Or expression.
  514. f"id:{replay1_id} OR id:{uuid.uuid4().hex} OR id:{uuid.uuid4().hex}",
  515. # Paren wrapped expression.
  516. f"((id:{replay1_id} OR id:b) AND (duration:>15 OR id:d))",
  517. # Implicit paren wrapped expression.
  518. f"(id:{replay1_id} OR id:b) AND (duration:>15 OR id:d)",
  519. # Implicit And.
  520. f"(id:{replay1_id} OR id:b) OR (duration:>15 platform:javascript)",
  521. # Tag filters.
  522. "tags[a]:m",
  523. "a:m",
  524. "c:*st",
  525. "!c:*zz",
  526. "urls:example.com",
  527. "url:example.com",
  528. "activity:3",
  529. "activity:>2",
  530. ]
  531. for query in queries:
  532. response = self.client.get(self.url + f"?field=id&query={query}")
  533. assert response.status_code == 200, query
  534. response_data = response.json()
  535. assert len(response_data["data"]) == 1, query
  536. # Test all queries as a single AND expression.
  537. all_queries = " ".join(queries)
  538. response = self.client.get(self.url + f"?query={all_queries}")
  539. assert response.status_code == 200
  540. response_data = response.json()
  541. assert len(response_data["data"]) == 1, "all queries"
  542. # Assert returns empty result sets.
  543. null_queries = [
  544. "!replay_type:session",
  545. "!error_ids:a3a62ef6ac86415b83c2416fc2f76db1",
  546. "error_ids:123",
  547. "!error_id:a3a62ef6ac86415b83c2416fc2f76db1",
  548. "error_id:123",
  549. "!trace_ids:4491657243ba4dbebd2f6bd62b733080",
  550. "!trace_id:4491657243ba4dbebd2f6bd62b733080",
  551. "!trace:4491657243ba4dbebd2f6bd62b733080",
  552. "count_urls:0",
  553. "count_dead_clicks:>0",
  554. "count_rage_clicks:>0",
  555. f"id:{replay1_id} AND id:b",
  556. f"id:{replay1_id} AND duration:>1000",
  557. "id:b OR duration:>1000",
  558. "a:o",
  559. "a:[o,p]",
  560. "releases:a",
  561. "releases:*4",
  562. "!releases:*3",
  563. "releases:[a,b]",
  564. "release:a",
  565. "release:*4",
  566. "!release:*3",
  567. "release:[a,b]",
  568. "c:*zz",
  569. "!c:*st",
  570. "!activity:3",
  571. "activity:<2",
  572. ]
  573. for query in null_queries:
  574. response = self.client.get(self.url + f"?field=id&query={query}")
  575. assert response.status_code == 200, query
  576. response_data = response.json()
  577. assert len(response_data["data"]) == 0, query
  578. def test_get_replays_user_sorts(self):
  579. """Test replays conform to the interchange format."""
  580. project = self.create_project(teams=[self.team])
  581. project2 = self.create_project(teams=[self.team])
  582. replay1_id = uuid.uuid4().hex
  583. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=15)
  584. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  585. self.store_replays(
  586. mock_replay(
  587. seq1_timestamp,
  588. project2.id,
  589. replay1_id,
  590. error_ids=[uuid.uuid4().hex, uuid.uuid4().hex],
  591. platform="b",
  592. dist="b",
  593. user_id="b",
  594. user_email="b",
  595. user_name="b",
  596. user_ip_address="127.0.0.2",
  597. sdk_name="b",
  598. sdk_version="b",
  599. os_name="b",
  600. os_version="b",
  601. browser_name="b",
  602. browser_version="b",
  603. device_name="b",
  604. device_brand="b",
  605. device_family="b",
  606. device_model="b",
  607. segment_id=0,
  608. )
  609. )
  610. self.store_replays(
  611. mock_replay(
  612. seq2_timestamp,
  613. project2.id,
  614. replay1_id,
  615. platform="b",
  616. dist="b",
  617. user_id="b",
  618. user_email="b",
  619. user_name="b",
  620. user_ip_address="127.0.0.2",
  621. sdk_name="b",
  622. sdk_version="b",
  623. os_name="b",
  624. os_version="b",
  625. browser_name="b",
  626. browser_version="b",
  627. device_name="b",
  628. device_brand="b",
  629. device_family="b",
  630. device_model="b",
  631. segment_id=1,
  632. )
  633. )
  634. replay2_id = uuid.uuid4().hex
  635. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=15)
  636. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=10)
  637. self.store_replays(
  638. mock_replay(
  639. seq1_timestamp,
  640. project.id,
  641. replay2_id,
  642. error_ids=[uuid.uuid4().hex],
  643. platform="a",
  644. dist="a",
  645. user_id="a",
  646. user_email="a",
  647. user_name="a",
  648. user_ip_address="127.0.0.1",
  649. sdk_name="a",
  650. sdk_version="a",
  651. os_name="a",
  652. os_version="a",
  653. browser_name="a",
  654. browser_version="a",
  655. device_name="a",
  656. device_brand="a",
  657. device_family="a",
  658. device_model="a",
  659. segment_id=0,
  660. )
  661. )
  662. self.store_replays(
  663. mock_replay(
  664. seq2_timestamp,
  665. project.id,
  666. replay2_id,
  667. platform="a",
  668. dist="a",
  669. user_id="a",
  670. user_email="a",
  671. user_name="a",
  672. user_ip_address="127.0.0.1",
  673. sdk_name="a",
  674. sdk_version="a",
  675. os_name="a",
  676. os_version="a",
  677. browser_name="a",
  678. browser_version="a",
  679. device_name="a",
  680. device_brand="a",
  681. device_family="a",
  682. device_model="a",
  683. segment_id=1,
  684. )
  685. )
  686. with self.feature(REPLAYS_FEATURES):
  687. # Run all the queries individually to determine compliance.
  688. queries = [
  689. "activity",
  690. "browser.name",
  691. "browser.version",
  692. "device.brand",
  693. "device.family",
  694. "device.model",
  695. "device.name",
  696. "dist",
  697. "duration",
  698. "os.name",
  699. "os.version",
  700. "platform",
  701. "project_id",
  702. "sdk.name",
  703. "user.email",
  704. "user.id",
  705. "user.username",
  706. ]
  707. for key in queries:
  708. # Ascending
  709. response = self.client.get(self.url + f"?sort={key}")
  710. assert response.status_code == 200, key
  711. r = response.json()
  712. assert len(r["data"]) == 2, key
  713. assert r["data"][0]["id"] == replay2_id, key
  714. assert r["data"][1]["id"] == replay1_id, key
  715. # Descending
  716. response = self.client.get(self.url + f"?sort=-{key}")
  717. assert response.status_code == 200, key
  718. r = response.json()
  719. assert len(r["data"]) == 2, key
  720. assert r["data"][0]["id"] == replay1_id, key
  721. assert r["data"][1]["id"] == replay2_id, key
  722. # No such thing as a bad field with the tag filtering behavior.
  723. #
  724. # def test_get_replays_filter_bad_field(self):
  725. # """Test replays conform to the interchange format."""
  726. # self.create_project(teams=[self.team])
  727. # with self.feature(REPLAYS_FEATURES):
  728. # response = self.client.get(self.url + "?query=xyz:a")
  729. # assert response.status_code == 400
  730. # assert b"xyz" in response.content
  731. def test_get_replays_filter_bad_value(self):
  732. """Test replays conform to the interchange format."""
  733. self.create_project(teams=[self.team])
  734. with self.feature(REPLAYS_FEATURES):
  735. response = self.client.get(self.url + "?query=duration:a")
  736. assert response.status_code == 400
  737. assert b"duration" in response.content
  738. def test_get_replays_no_multi_project_select(self):
  739. self.create_project(teams=[self.team])
  740. self.create_project(teams=[self.team])
  741. user = self.create_user(is_superuser=False)
  742. self.create_member(
  743. user=user, organization=self.organization, role="member", teams=[self.team]
  744. )
  745. self.login_as(user)
  746. with self.feature(REPLAYS_FEATURES), self.feature({"organizations:global-views": False}):
  747. response = self.client.get(self.url)
  748. assert response.status_code == 400
  749. assert response.data["detail"] == "You cannot view events from multiple projects."
  750. def test_get_replays_no_multi_project_select_query_referrer(self):
  751. self.create_project(teams=[self.team])
  752. self.create_project(teams=[self.team])
  753. user = self.create_user(is_superuser=False)
  754. self.create_member(
  755. user=user, organization=self.organization, role="member", teams=[self.team]
  756. )
  757. self.login_as(user)
  758. with self.feature(REPLAYS_FEATURES), self.feature({"organizations:global-views": False}):
  759. response = self.client.get(self.url + "?queryReferrer=issueReplays")
  760. assert response.status_code == 200
  761. def test_get_replays_unknown_field(self):
  762. """Test replays unknown fields raise a 400 error."""
  763. project = self.create_project(teams=[self.team])
  764. replay1_id = uuid.uuid4().hex
  765. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  766. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  767. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  768. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  769. with self.feature(REPLAYS_FEATURES):
  770. response = self.client.get(self.url + "?field=unknown")
  771. assert response.status_code == 400
  772. def test_get_replays_activity_field(self):
  773. """Test replays activity field does not raise 400."""
  774. project = self.create_project(teams=[self.team])
  775. replay1_id = uuid.uuid4().hex
  776. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  777. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  778. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  779. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  780. with self.feature(REPLAYS_FEATURES):
  781. response = self.client.get(self.url + "?field=activity")
  782. assert response.status_code == 200
  783. def test_archived_records_are_null_fields(self):
  784. replay1_id = uuid.uuid4().hex
  785. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=30)
  786. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=15)
  787. self.store_replays(mock_replay(seq1_timestamp, self.project.id, replay1_id))
  788. self.store_replays(
  789. mock_replay(seq2_timestamp, self.project.id, replay1_id, is_archived=True)
  790. )
  791. with self.feature(REPLAYS_FEATURES):
  792. response = self.client.get(self.url)
  793. assert response.status_code == 200
  794. assert response.json()["data"] == [
  795. {
  796. "id": replay1_id,
  797. "project_id": str(self.project.id),
  798. "trace_ids": [],
  799. "error_ids": [],
  800. "environment": None,
  801. "tags": [],
  802. "user": {"id": "Archived Replay", "display_name": "Archived Replay"},
  803. "sdk": {"name": None, "version": None},
  804. "os": {"name": None, "version": None},
  805. "browser": {"name": None, "version": None},
  806. "device": {"name": None, "brand": None, "model": None, "family": None},
  807. "urls": None,
  808. "started_at": None,
  809. "count_errors": None,
  810. "count_dead_clicks": None,
  811. "count_rage_clicks": None,
  812. "activity": None,
  813. "finished_at": None,
  814. "duration": None,
  815. "is_archived": True,
  816. "releases": None,
  817. "platform": None,
  818. "dist": None,
  819. "count_segments": None,
  820. "count_urls": None,
  821. "clicks": None,
  822. }
  823. ]
  824. # commented out until https://github.com/getsentry/snuba/pull/4137 is merged.
  825. # def test_archived_records_out_of_bounds(self):
  826. # replay1_id = uuid.uuid4().hex
  827. # seq1_timestamp = datetime.datetime.now() - datetime.timedelta(days=10)
  828. # seq2_timestamp = datetime.datetime.now() - datetime.timedelta(days=3)
  829. # self.store_replays(mock_replay(seq1_timestamp, self.project.id, replay1_id))
  830. # self.store_replays(
  831. # mock_replay(
  832. # seq2_timestamp, self.project.id, replay1_id, is_archived=True, segment_id=None
  833. # )
  834. # )
  835. # with self.feature(REPLAYS_FEATURES):
  836. # response = self.client.get(self.url)
  837. # assert response.status_code == 200
  838. # assert response.json()["data"] == [
  839. # {
  840. # "id": replay1_id,
  841. # "project_id": str(self.project.id),
  842. # "trace_ids": [],
  843. # "error_ids": [],
  844. # "environment": None,
  845. # "tags": [],
  846. # "user": {"id": "Archived Replay", "display_name": "Archived Replay"},
  847. # "sdk": {"name": None, "version": None},
  848. # "os": {"name": None, "version": None},
  849. # "browser": {"name": None, "version": None},
  850. # "device": {"name": None, "brand": None, "model": None, "family": None},
  851. # "urls": None,
  852. # "started_at": None,
  853. # "count_errors": None,
  854. # "activity": None,
  855. # "finished_at": None,
  856. # "duration": None,
  857. # "is_archived": True,
  858. # }
  859. # ]
  860. def test_get_replays_filter_clicks(self):
  861. """Test replays conform to the interchange format."""
  862. project = self.create_project(teams=[self.team])
  863. replay1_id = uuid.uuid4().hex
  864. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  865. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  866. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  867. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  868. self.store_replays(
  869. mock_replay_click(
  870. seq2_timestamp,
  871. project.id,
  872. replay1_id,
  873. node_id=1,
  874. tag="div",
  875. id="myid",
  876. class_=["class1", "class2"],
  877. role="button",
  878. testid="1",
  879. alt="Alt",
  880. aria_label="AriaLabel",
  881. title="MyTitle",
  882. text="Hello",
  883. )
  884. )
  885. self.store_replays(
  886. mock_replay_click(
  887. seq2_timestamp,
  888. project.id,
  889. replay1_id,
  890. node_id=2,
  891. tag="button",
  892. id="myid",
  893. class_=["class1", "class3"],
  894. )
  895. )
  896. with self.feature(REPLAYS_FEATURES):
  897. queries = [
  898. "click.alt:Alt",
  899. "click.class:class1",
  900. "click.class:class2",
  901. "click.class:class3",
  902. "click.id:myid",
  903. "click.label:AriaLabel",
  904. "click.role:button",
  905. "click.tag:div",
  906. "click.tag:button",
  907. "click.testid:1",
  908. "click.textContent:Hello",
  909. "click.title:MyTitle",
  910. "click.selector:div#myid",
  911. "click.selector:div[alt=Alt]",
  912. "click.selector:div[title=MyTitle]",
  913. "click.selector:div[data-testid='1']",
  914. "click.selector:div[data-test-id='1']",
  915. "click.selector:div[role=button]",
  916. "click.selector:div#myid.class1.class2",
  917. # Single quotes around attribute value.
  918. "click.selector:div[role='button']",
  919. "click.selector:div#myid.class1.class2[role=button][aria-label='AriaLabel']",
  920. ]
  921. for query in queries:
  922. response = self.client.get(self.url + f"?field=id&query={query}")
  923. assert response.status_code == 200, query
  924. response_data = response.json()
  925. assert len(response_data["data"]) == 1, query
  926. queries = [
  927. "click.alt:NotAlt",
  928. "click.class:class4",
  929. "click.id:other",
  930. "click.label:NotAriaLabel",
  931. "click.role:form",
  932. "click.tag:header",
  933. "click.testid:2",
  934. "click.textContent:World",
  935. "click.title:NotMyTitle",
  936. "!click.selector:div#myid",
  937. "click.selector:div#notmyid",
  938. # Assert all classes must match.
  939. "click.selector:div#myid.class1.class2.class3",
  940. # Invalid selectors return no rows.
  941. "click.selector:$#%^#%",
  942. # Integer type role values are not allowed and must be wrapped in single quotes.
  943. "click.selector:div[title=1]",
  944. ]
  945. for query in queries:
  946. response = self.client.get(self.url + f"?query={query}")
  947. assert response.status_code == 200, query
  948. response_data = response.json()
  949. assert len(response_data["data"]) == 0, query
  950. def test_get_replays_click_fields(self):
  951. project = self.create_project(teams=[self.team])
  952. replay1_id = uuid.uuid4().hex
  953. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  954. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  955. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  956. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  957. self.store_replays(
  958. mock_replay_click(
  959. seq2_timestamp,
  960. project.id,
  961. replay1_id,
  962. node_id=1,
  963. tag="div",
  964. id="myid",
  965. class_=["class1", "class2"],
  966. role="button",
  967. testid="1",
  968. alt="Alt",
  969. aria_label="AriaLabel",
  970. title="MyTitle",
  971. text="Hello",
  972. )
  973. )
  974. self.store_replays(
  975. mock_replay_click(
  976. seq2_timestamp,
  977. project.id,
  978. replay1_id,
  979. node_id=2,
  980. tag="button",
  981. id="myid",
  982. class_=["class1", "class3"],
  983. )
  984. )
  985. with self.feature(REPLAYS_FEATURES):
  986. response = self.client.get(self.url + "?field=clicks")
  987. assert response.status_code == 200, response.content
  988. response_data = response.json()
  989. assert response_data["data"] == [
  990. {
  991. "clicks": [
  992. {
  993. "click.alt": "Alt",
  994. "click.classes": ["class1", "class3"],
  995. "click.id": "myid",
  996. "click.role": "button",
  997. "click.tag": "button",
  998. "click.testid": "1",
  999. "click.text": "Hello",
  1000. "click.title": "MyTitle",
  1001. "click.label": "AriaLabel",
  1002. },
  1003. {
  1004. "click.alt": None,
  1005. "click.classes": ["class1", "class2"],
  1006. "click.id": "myid",
  1007. "click.role": None,
  1008. "click.tag": "div",
  1009. "click.testid": None,
  1010. "click.text": None,
  1011. "click.title": None,
  1012. "click.label": None,
  1013. },
  1014. ]
  1015. }
  1016. ]
  1017. def test_get_replays_filter_clicks_nested_selector(self):
  1018. """Test replays do not support nested selectors."""
  1019. project = self.create_project(teams=[self.team])
  1020. self.store_replays(mock_replay(datetime.datetime.now(), project.id, uuid.uuid4().hex))
  1021. with self.feature(REPLAYS_FEATURES):
  1022. queries = [
  1023. 'click.selector:"div button"',
  1024. 'click.selector:"div + button"',
  1025. 'click.selector:"div ~ button"',
  1026. 'click.selector:"div > button"',
  1027. ]
  1028. for query in queries:
  1029. response = self.client.get(self.url + f"?field=id&query={query}")
  1030. assert response.status_code == 400
  1031. assert response.content == b'{"detail":"Nested selectors are not supported."}'
  1032. def test_get_replays_filter_clicks_pseudo_element(self):
  1033. """Assert replays only supports a subset of selector syntax."""
  1034. project = self.create_project(teams=[self.team])
  1035. self.store_replays(mock_replay(datetime.datetime.now(), project.id, uuid.uuid4().hex))
  1036. with self.feature(REPLAYS_FEATURES):
  1037. queries = [
  1038. "click.selector:a::visited",
  1039. ]
  1040. for query in queries:
  1041. response = self.client.get(self.url + f"?field=id&query={query}")
  1042. assert response.status_code == 400, query
  1043. assert response.content == b'{"detail":"Pseudo-elements are not supported."}', query
  1044. def test_get_replays_filter_clicks_unsupported_selector(self):
  1045. """Assert replays only supports a subset of selector syntax."""
  1046. project = self.create_project(teams=[self.team])
  1047. self.store_replays(mock_replay(datetime.datetime.now(), project.id, uuid.uuid4().hex))
  1048. with self.feature(REPLAYS_FEATURES):
  1049. queries = [
  1050. "click.selector:div:is(2)",
  1051. "click.selector:p:active",
  1052. ]
  1053. for query in queries:
  1054. response = self.client.get(self.url + f"?field=id&query={query}")
  1055. assert response.status_code == 400, query
  1056. assert (
  1057. response.content
  1058. == b'{"detail":"Only attribute, class, id, and tag name selectors are supported."}'
  1059. ), query
  1060. def test_get_replays_filter_clicks_unsupported_attribute_selector(self):
  1061. """Assert replays only supports a subset of selector syntax."""
  1062. project = self.create_project(teams=[self.team])
  1063. self.store_replays(mock_replay(datetime.datetime.now(), project.id, uuid.uuid4().hex))
  1064. with self.feature(REPLAYS_FEATURES):
  1065. queries = ["click.selector:div[xyz=test]"]
  1066. for query in queries:
  1067. response = self.client.get(self.url + f"?field=id&query={query}")
  1068. assert response.status_code == 400, query
  1069. assert response.content == (
  1070. b'{"detail":"Invalid attribute specified. Only alt, aria-label, role, '
  1071. b'data-testid, data-test-id, and title are supported."}'
  1072. ), query
  1073. def test_get_replays_filter_clicks_unsupported_operators(self):
  1074. """Assert replays only supports a subset of selector syntax."""
  1075. project = self.create_project(teams=[self.team])
  1076. self.store_replays(mock_replay(datetime.datetime.now(), project.id, uuid.uuid4().hex))
  1077. with self.feature(REPLAYS_FEATURES):
  1078. queries = [
  1079. 'click.selector:"[aria-label~=button]"',
  1080. 'click.selector:"[aria-label|=button]"',
  1081. 'click.selector:"[aria-label^=button]"',
  1082. 'click.selector:"[aria-label$=button]"',
  1083. ]
  1084. for query in queries:
  1085. response = self.client.get(self.url + f"?field=id&query={query}")
  1086. assert response.status_code == 400, query
  1087. assert (
  1088. response.content == b'{"detail":"Only the \'=\' operator is supported."}'
  1089. ), query
  1090. def test_get_replays_field_order(self):
  1091. """Test replay response with fields requested in production."""
  1092. project = self.create_project(teams=[self.team])
  1093. replay1_id = uuid.uuid4().hex
  1094. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1095. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1096. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1097. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1098. with self.feature(REPLAYS_FEATURES):
  1099. # Invalid field-names error regardless of ordering.
  1100. response = self.client.get(self.url + "?field=invalid&field=browser")
  1101. assert response.status_code == 400
  1102. response = self.client.get(self.url + "?field=browser&field=invalid")
  1103. assert response.status_code == 400
  1104. # Correct field-names never error.
  1105. response = self.client.get(self.url + "?field=count_urls&field=browser")
  1106. assert response.status_code == 200
  1107. response = self.client.get(self.url + "?field=browser&field=count_urls")
  1108. assert response.status_code == 200
  1109. def test_get_replays_memory_error(self):
  1110. """Test replay response with fields requested in production."""
  1111. project = self.create_project(teams=[self.team])
  1112. replay1_id = uuid.uuid4().hex
  1113. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1114. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1115. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1116. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1117. with self.feature(REPLAYS_FEATURES):
  1118. # Invalid field-names error regardless of ordering.
  1119. with mock.patch(
  1120. "sentry.replays.endpoints.organization_replay_index.query_replays_collection",
  1121. side_effect=QueryMemoryLimitExceeded("mocked error"),
  1122. ):
  1123. response = self.client.get(self.url)
  1124. assert response.status_code == 400
  1125. assert (
  1126. response.content
  1127. == b'{"detail":"Replay search query limits exceeded. Please narrow the time-range."}'
  1128. )
  1129. @pytest.mark.skip(reason="flaky: Date logic breaks - possibly due to stats-period.")
  1130. def test_get_replays_dead_rage_click_cutoff(self):
  1131. """Test rage and dead clicks are accumulated after the cutoff."""
  1132. project = self.create_project(teams=[self.team])
  1133. replay1_id = uuid.uuid4().hex
  1134. pre_cutoff = datetime.datetime(year=2023, month=7, day=23)
  1135. post_cutoff = datetime.datetime(year=2023, month=7, day=24)
  1136. self.store_replays(
  1137. mock_replay(
  1138. pre_cutoff,
  1139. project.id,
  1140. replay1_id,
  1141. )
  1142. )
  1143. self.store_replays(
  1144. mock_replay(
  1145. post_cutoff,
  1146. project.id,
  1147. replay1_id,
  1148. )
  1149. )
  1150. self.store_replays(
  1151. mock_replay_click(
  1152. pre_cutoff,
  1153. project.id,
  1154. replay1_id,
  1155. node_id=1,
  1156. tag="div",
  1157. id="myid",
  1158. class_=["class1", "class2"],
  1159. role="button",
  1160. testid="1",
  1161. alt="Alt",
  1162. aria_label="AriaLabel",
  1163. title="MyTitle",
  1164. is_dead=1,
  1165. is_rage=1,
  1166. text="Hello",
  1167. )
  1168. )
  1169. self.store_replays(
  1170. mock_replay_click(
  1171. post_cutoff,
  1172. project.id,
  1173. replay1_id,
  1174. node_id=1,
  1175. tag="div",
  1176. id="myid",
  1177. class_=["class1", "class2"],
  1178. role="button",
  1179. testid="1",
  1180. alt="Alt",
  1181. aria_label="AriaLabel",
  1182. title="MyTitle",
  1183. is_dead=1,
  1184. is_rage=1,
  1185. text="Hello",
  1186. )
  1187. )
  1188. with self.feature(REPLAYS_FEATURES):
  1189. response = self.client.get(
  1190. self.url
  1191. + f"?start={pre_cutoff.isoformat().split('.')[0]}&end={post_cutoff.isoformat().split('.')[0]}"
  1192. )
  1193. assert response.status_code == 200
  1194. response_data = response.json()
  1195. assert "data" in response_data
  1196. assert len(response_data["data"]) == 1
  1197. item = response_data["data"][0]
  1198. assert item["count_dead_clicks"] == 1, item["count_dead_clicks"]
  1199. assert item["count_rage_clicks"] == 1, item["count_rage_clicks"]
  1200. @apply_feature_flag_on_cls("organizations:session-replay-optimized-search")
  1201. class OrganizationReplayIndexOptimizedSearchTest(OrganizationReplayIndexTest):
  1202. # Currently only available on the newest query engine so the test is defined within this
  1203. # subclass.
  1204. def test_get_replays_filter_clicks_non_click_rows(self):
  1205. project = self.create_project(teams=[self.team])
  1206. replay1_id = uuid.uuid4().hex
  1207. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1208. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1209. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1210. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1211. self.store_replays(
  1212. mock_replay_click(
  1213. seq2_timestamp,
  1214. project.id,
  1215. replay1_id,
  1216. node_id=1,
  1217. tag="div",
  1218. id="id1",
  1219. class_=["id1"],
  1220. text="id1",
  1221. role="id1",
  1222. alt="id1",
  1223. testid="id1",
  1224. aria_label="id1",
  1225. title="id1",
  1226. )
  1227. )
  1228. self.store_replays(
  1229. mock_replay_click(
  1230. seq2_timestamp,
  1231. project.id,
  1232. replay1_id,
  1233. node_id=2,
  1234. tag="",
  1235. id="id2",
  1236. class_=["id2"],
  1237. text="id2",
  1238. role="id2",
  1239. alt="id2",
  1240. testid="id2",
  1241. aria_label="id2",
  1242. title="id2",
  1243. )
  1244. )
  1245. with self.feature(REPLAYS_FEATURES):
  1246. success_queries = [
  1247. "click.id:id1",
  1248. "click.class:[id1]",
  1249. "click.textContent:id1",
  1250. "click.role:id1",
  1251. "click.alt:id1",
  1252. "click.testid:id1",
  1253. "click.label:id1",
  1254. "click.title:id1",
  1255. ]
  1256. for query in success_queries:
  1257. response = self.client.get(self.url + f"?field=id&query={query}")
  1258. assert response.status_code == 200
  1259. response_data = response.json()
  1260. assert len(response_data["data"]) == 1, query
  1261. # These tests demonstrate what happens when you match a click value on non-click row.
  1262. failure_queries = [
  1263. "click.id:id2",
  1264. "click.class:[id2]",
  1265. "click.textContent:id2",
  1266. "click.role:id2",
  1267. "click.alt:id2",
  1268. "click.testid:id2",
  1269. "click.label:id2",
  1270. "click.title:id2",
  1271. ]
  1272. for query in failure_queries:
  1273. response = self.client.get(self.url + f"?field=id&query={query}")
  1274. assert response.status_code == 200
  1275. response_data = response.json()
  1276. assert len(response_data["data"]) == 0, query
  1277. # The following section tests the valid branches of the condition classes.
  1278. def test_query_branches_string_conditions(self):
  1279. project = self.create_project(teams=[self.team])
  1280. replay1_id = uuid.uuid4().hex
  1281. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1282. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1283. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1284. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1285. with self.feature(REPLAYS_FEATURES):
  1286. queries = [
  1287. "device.brand:Apple",
  1288. "!device.brand:Microsoft",
  1289. "device.brand:[Apple,Microsoft]",
  1290. "!device.brand:[Oracle,Microsoft]",
  1291. "device.brand:App*",
  1292. "!device.brand:Micro*",
  1293. ]
  1294. for query in queries:
  1295. response = self.client.get(self.url + f"?field=id&query={query}")
  1296. assert response.status_code == 200
  1297. response_data = response.json()
  1298. assert len(response_data["data"]) == 1, query
  1299. def test_query_branches_click_scalar_conditions(self):
  1300. project = self.create_project(teams=[self.team])
  1301. replay1_id = uuid.uuid4().hex
  1302. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1303. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1304. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1305. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1306. self.store_replays(
  1307. mock_replay_click(
  1308. seq2_timestamp, project.id, replay1_id, node_id=1, tag="div", id="id1"
  1309. )
  1310. )
  1311. with self.feature(REPLAYS_FEATURES):
  1312. queries = [
  1313. "click.id:id1",
  1314. "!click.id:id2",
  1315. "click.id:[id1,id2]",
  1316. "!click.id:[id3,id2]",
  1317. "click.id:*1",
  1318. "!click.id:*2",
  1319. ]
  1320. for query in queries:
  1321. response = self.client.get(self.url + f"?field=id&query={query}")
  1322. assert response.status_code == 200
  1323. response_data = response.json()
  1324. assert len(response_data["data"]) == 1, query
  1325. def test_query_branches_click_array_conditions(self):
  1326. project = self.create_project(teams=[self.team])
  1327. replay1_id = uuid.uuid4().hex
  1328. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1329. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1330. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1331. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1332. self.store_replays(
  1333. mock_replay_click(
  1334. seq2_timestamp, project.id, replay1_id, node_id=1, tag="div", class_=["class1"]
  1335. )
  1336. )
  1337. with self.feature(REPLAYS_FEATURES):
  1338. queries = [
  1339. "click.class:class1",
  1340. "!click.class:class2",
  1341. "click.class:[class1,class2]",
  1342. "!click.class:[class3,class2]",
  1343. "click.class:*1",
  1344. "!click.class:*2",
  1345. ]
  1346. for query in queries:
  1347. response = self.client.get(self.url + f"?field=id&query={query}")
  1348. assert response.status_code == 200
  1349. response_data = response.json()
  1350. assert len(response_data["data"]) == 1, query
  1351. def test_query_branches_array_of_string_conditions(self):
  1352. project = self.create_project(teams=[self.team])
  1353. replay1_id = uuid.uuid4().hex
  1354. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1355. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1356. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id, urls=["Apple"]))
  1357. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id, urls=[]))
  1358. with self.feature(REPLAYS_FEATURES):
  1359. queries = [
  1360. "urls:Apple",
  1361. "!urls:Microsoft",
  1362. "urls:[Apple,Microsoft]",
  1363. "!urls:[Oracle,Microsoft]",
  1364. "urls:App*",
  1365. "!urls:Micro*",
  1366. ]
  1367. for query in queries:
  1368. response = self.client.get(self.url + f"?field=id&query={query}")
  1369. assert response.status_code == 200
  1370. response_data = response.json()
  1371. assert len(response_data["data"]) == 1, query
  1372. def test_query_branches_integer_conditions(self):
  1373. project = self.create_project(teams=[self.team])
  1374. replay1_id = uuid.uuid4().hex
  1375. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1376. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1377. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1378. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id, error_ids=[]))
  1379. with self.feature(REPLAYS_FEATURES):
  1380. queries = [
  1381. "count_errors:1",
  1382. "!count_errors:2",
  1383. "count_errors:>0",
  1384. "count_errors:<2",
  1385. "count_errors:>=1",
  1386. "count_errors:<=1",
  1387. "count_errors:[1,2]",
  1388. "!count_errors:[2,3]",
  1389. ]
  1390. for query in queries:
  1391. response = self.client.get(self.url + f"?field=id&query={query}")
  1392. assert response.status_code == 200
  1393. response_data = response.json()
  1394. assert len(response_data["data"]) == 1, query
  1395. def test_query_branches_error_ids_conditions(self):
  1396. project = self.create_project(teams=[self.team])
  1397. uid1 = uuid.uuid4().hex
  1398. uid2 = uuid.uuid4().hex
  1399. replay1_id = uuid.uuid4().hex
  1400. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1401. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1402. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id, error_ids=[uid1]))
  1403. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1404. with self.feature(REPLAYS_FEATURES):
  1405. queries = [
  1406. f"error_ids:{uid1}",
  1407. f"!error_ids:{uid2}",
  1408. f"error_ids:[{uid1},{uid2}]",
  1409. f"!error_ids:[{uid2}]",
  1410. ]
  1411. for query in queries:
  1412. response = self.client.get(self.url + f"?field=id&query={query}")
  1413. assert response.status_code == 200
  1414. response_data = response.json()
  1415. assert len(response_data["data"]) == 1, query
  1416. def test_query_branches_uuid_conditions(self):
  1417. project = self.create_project(teams=[self.team])
  1418. uid1 = uuid.uuid4().hex
  1419. uid2 = uuid.uuid4().hex
  1420. replay1_id = uuid.uuid4().hex
  1421. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1422. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1423. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id, trace_ids=[uid1]))
  1424. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1425. with self.feature(REPLAYS_FEATURES):
  1426. queries = [
  1427. f"trace_ids:{uid1}",
  1428. f"!trace_ids:{uid2}",
  1429. f"trace_ids:[{uid1},{uid2}]",
  1430. f"!trace_ids:[{uid2}]",
  1431. ]
  1432. for query in queries:
  1433. response = self.client.get(self.url + f"?field=id&query={query}")
  1434. assert response.status_code == 200
  1435. response_data = response.json()
  1436. assert len(response_data["data"]) == 1, query
  1437. def test_query_branches_string_uuid_conditions(self):
  1438. project = self.create_project(teams=[self.team])
  1439. replay1_id = uuid.uuid4().hex
  1440. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1441. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1442. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1443. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1444. with self.feature(REPLAYS_FEATURES):
  1445. uid2 = uuid.uuid4().hex
  1446. queries = [
  1447. f"id:{replay1_id}",
  1448. f"!id:{uid2}",
  1449. f"id:[{replay1_id},{uid2}]",
  1450. f"!id:[{uid2}]",
  1451. ]
  1452. for query in queries:
  1453. response = self.client.get(self.url + f"?field=id&query={query}")
  1454. assert response.status_code == 200
  1455. response_data = response.json()
  1456. assert len(response_data["data"]) == 1, query
  1457. def test_query_branches_ip_address_conditions(self):
  1458. project = self.create_project(teams=[self.team])
  1459. replay1_id = uuid.uuid4().hex
  1460. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1461. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1462. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1463. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id))
  1464. with self.feature(REPLAYS_FEATURES):
  1465. queries = [
  1466. "user.ip_address:127.0.0.1",
  1467. "!user.ip_address:192.168.0.1",
  1468. "user.ip_address:[127.0.0.1,192.168.0.1]",
  1469. "!user.ip_address:[192.168.0.1]",
  1470. ]
  1471. for query in queries:
  1472. response = self.client.get(self.url + f"?field=id&query={query}")
  1473. assert response.status_code == 200
  1474. response_data = response.json()
  1475. assert len(response_data["data"]) == 1, query
  1476. def test_query_branches_computed_activity_conditions(self):
  1477. project = self.create_project(teams=[self.team])
  1478. replay1_id = uuid.uuid4().hex
  1479. seq1_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=22)
  1480. seq2_timestamp = datetime.datetime.now() - datetime.timedelta(seconds=5)
  1481. self.store_replays(mock_replay(seq1_timestamp, project.id, replay1_id))
  1482. self.store_replays(mock_replay(seq2_timestamp, project.id, replay1_id, error_ids=[]))
  1483. with self.feature(REPLAYS_FEATURES):
  1484. queries = [
  1485. "activity:2",
  1486. "!activity:1",
  1487. "activity:>1",
  1488. "activity:<3",
  1489. "activity:>=2",
  1490. "activity:<=2",
  1491. "activity:[1,2]",
  1492. "!activity:[1,3]",
  1493. ]
  1494. for query in queries:
  1495. response = self.client.get(self.url + f"?field=id&query={query}")
  1496. assert response.status_code == 200
  1497. response_data = response.json()
  1498. assert len(response_data["data"]) == 1, query