test_tsdb_backend.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  1. from datetime import UTC, datetime, timedelta
  2. from unittest.mock import patch
  3. from snuba_sdk import Limit
  4. from sentry.issues.grouptype import ProfileFileIOGroupType
  5. from sentry.models.environment import Environment
  6. from sentry.models.group import Group
  7. from sentry.models.grouprelease import GroupRelease
  8. from sentry.models.release import Release
  9. from sentry.testutils.cases import SnubaTestCase, TestCase
  10. from sentry.testutils.helpers.datetime import before_now, iso_format
  11. from sentry.tsdb.base import TSDBModel
  12. from sentry.tsdb.snuba import SnubaTSDB
  13. from sentry.utils.dates import to_datetime
  14. from tests.sentry.issues.test_utils import SearchIssueTestMixin
  15. def timestamp(d):
  16. t = int(d.timestamp())
  17. return t - (t % 3600)
  18. def has_shape(data, shape, allow_empty=False):
  19. """
  20. Determine if a data object has the provided shape
  21. At any level, the object in `data` and in `shape` must have the same type.
  22. A dict is the same shape if all its keys and values have the same shape as the
  23. key/value in `shape`. The number of keys/values is not relevant.
  24. A list is the same shape if all its items have the same shape as the value
  25. in `shape`
  26. A tuple is the same shape if it has the same length as `shape` and all the
  27. values have the same shape as the corresponding value in `shape`
  28. Any other object simply has to have the same type.
  29. If `allow_empty` is set, lists and dicts in `data` will pass even if they are empty.
  30. """
  31. if not isinstance(data, type(shape)):
  32. return False
  33. if isinstance(data, dict):
  34. return (
  35. (allow_empty or len(data) > 0)
  36. and all(has_shape(k, list(shape.keys())[0]) for k in data.keys())
  37. and all(has_shape(v, list(shape.values())[0]) for v in data.values())
  38. )
  39. elif isinstance(data, list):
  40. return (allow_empty or len(data) > 0) and all(has_shape(v, shape[0]) for v in data)
  41. elif isinstance(data, tuple):
  42. return len(data) == len(shape) and all(
  43. has_shape(data[i], shape[i]) for i in range(len(data))
  44. )
  45. else:
  46. return True
  47. class SnubaTSDBTest(TestCase, SnubaTestCase):
  48. def setUp(self):
  49. super().setUp()
  50. self.db = SnubaTSDB()
  51. self.now = before_now(hours=4).replace(hour=0, minute=0, second=0, microsecond=0)
  52. self.proj1 = self.create_project()
  53. env1 = "test"
  54. env2 = "dev"
  55. defaultenv = ""
  56. release1 = "1" * 10
  57. release2 = "2" * 10
  58. self.release1 = Release.objects.create(
  59. organization_id=self.organization.id, version=release1, date_added=self.now
  60. )
  61. self.release1.add_project(self.proj1)
  62. self.release2 = Release.objects.create(
  63. organization_id=self.organization.id, version=release2, date_added=self.now
  64. )
  65. self.release2.add_project(self.proj1)
  66. for r in range(0, 14400, 600): # Every 10 min for 4 hours
  67. self.store_event(
  68. data={
  69. "event_id": (str(r) * 32)[:32],
  70. "message": "message 1",
  71. "platform": "python",
  72. "fingerprint": [["group-1"], ["group-2"]][
  73. (r // 600) % 2
  74. ], # Switch every 10 mins
  75. "timestamp": iso_format(self.now + timedelta(seconds=r)),
  76. "tags": {
  77. "foo": "bar",
  78. "baz": "quux",
  79. "region": ["US", "EU"][(r // 7200) % 3],
  80. # Switch every 2 hours
  81. "environment": [env1, None][(r // 7200) % 3],
  82. "sentry:user": f"id:user{r // 3300}",
  83. },
  84. "user": {
  85. # change every 55 min so some hours have 1 user, some have 2
  86. "id": f"user{r // 3300}",
  87. },
  88. "release": str(r // 3600) * 10, # 1 per hour,
  89. },
  90. project_id=self.proj1.id,
  91. )
  92. groups = Group.objects.filter(project=self.proj1).order_by("id")
  93. self.proj1group1 = groups[0]
  94. self.proj1group2 = groups[1]
  95. self.env1 = Environment.objects.get(name=env1)
  96. self.env2 = self.create_environment(name=env2) # No events
  97. self.defaultenv = Environment.objects.get(name=defaultenv)
  98. self.group1release1env1 = GroupRelease.objects.get(
  99. project_id=self.proj1.id,
  100. group_id=self.proj1group1.id,
  101. release_id=self.release1.id,
  102. environment=env1,
  103. )
  104. self.group1release2env1 = GroupRelease.objects.create(
  105. project_id=self.proj1.id,
  106. group_id=self.proj1group1.id,
  107. release_id=self.release2.id,
  108. environment=env1,
  109. )
  110. self.group2release1env1 = GroupRelease.objects.get(
  111. project_id=self.proj1.id,
  112. group_id=self.proj1group2.id,
  113. release_id=self.release1.id,
  114. environment=env1,
  115. )
  116. def test_range_single(self):
  117. env1 = "test"
  118. project = self.create_project()
  119. for r in range(0, 600 * 6 * 4, 300): # Every 10 min for 4 hours
  120. self.store_event(
  121. data={
  122. "event_id": (str(r) * 32)[:32],
  123. "message": "message 1",
  124. "platform": "python",
  125. "fingerprint": ["group-1"],
  126. "timestamp": iso_format(self.now + timedelta(seconds=r)),
  127. "tags": {
  128. "foo": "bar",
  129. "baz": "quux",
  130. # Switch every 2 hours
  131. "region": "US",
  132. "environment": [env1, None][(r // 7200) % 3],
  133. "sentry:user": f"id:user{r // 3300}",
  134. },
  135. "user": {
  136. # change every 55 min so some hours have 1 user, some have 2
  137. "id": f"user{r // 3300}",
  138. },
  139. "release": str(r // 3600) * 10, # 1 per hour,
  140. },
  141. project_id=project.id,
  142. )
  143. groups = Group.objects.filter(project=project).order_by("id")
  144. group = groups[0]
  145. dts = [self.now + timedelta(hours=i) for i in range(4)]
  146. assert self.db.get_range(
  147. TSDBModel.group,
  148. [group.id],
  149. dts[0],
  150. dts[-1],
  151. rollup=3600,
  152. tenant_ids={"referrer": "r", "organization_id": 1234},
  153. ) == {
  154. group.id: [
  155. (timestamp(dts[0]), 6 * 2),
  156. (timestamp(dts[1]), 6 * 2),
  157. (timestamp(dts[2]), 6 * 2),
  158. (timestamp(dts[3]), 6 * 2),
  159. ]
  160. }
  161. def test_range_groups(self):
  162. dts = [self.now + timedelta(hours=i) for i in range(4)]
  163. assert self.db.get_range(
  164. TSDBModel.group,
  165. [self.proj1group1.id],
  166. dts[0],
  167. dts[-1],
  168. rollup=3600,
  169. tenant_ids={"referrer": "r", "organization_id": 1234},
  170. ) == {
  171. self.proj1group1.id: [
  172. (timestamp(dts[0]), 3),
  173. (timestamp(dts[1]), 3),
  174. (timestamp(dts[2]), 3),
  175. (timestamp(dts[3]), 3),
  176. ]
  177. }
  178. # Multiple groups
  179. assert self.db.get_range(
  180. TSDBModel.group,
  181. [self.proj1group1.id, self.proj1group2.id],
  182. dts[0],
  183. dts[-1],
  184. rollup=3600,
  185. tenant_ids={"referrer": "r", "organization_id": 1234},
  186. ) == {
  187. self.proj1group1.id: [
  188. (timestamp(dts[0]), 3),
  189. (timestamp(dts[1]), 3),
  190. (timestamp(dts[2]), 3),
  191. (timestamp(dts[3]), 3),
  192. ],
  193. self.proj1group2.id: [
  194. (timestamp(dts[0]), 3),
  195. (timestamp(dts[1]), 3),
  196. (timestamp(dts[2]), 3),
  197. (timestamp(dts[3]), 3),
  198. ],
  199. }
  200. assert (
  201. self.db.get_range(
  202. TSDBModel.group,
  203. [],
  204. dts[0],
  205. dts[-1],
  206. rollup=3600,
  207. tenant_ids={"referrer": "test", "organization_id": 1},
  208. )
  209. == {}
  210. )
  211. def test_range_releases(self):
  212. dts = [self.now + timedelta(hours=i) for i in range(4)]
  213. assert self.db.get_range(
  214. TSDBModel.release,
  215. [self.release1.id],
  216. dts[0],
  217. dts[-1],
  218. rollup=3600,
  219. tenant_ids={"referrer": "r", "organization_id": 1234},
  220. ) == {
  221. self.release1.id: [
  222. (timestamp(dts[0]), 0),
  223. (timestamp(dts[1]), 6),
  224. (timestamp(dts[2]), 0),
  225. (timestamp(dts[3]), 0),
  226. ]
  227. }
  228. def test_range_project(self):
  229. dts = [self.now + timedelta(hours=i) for i in range(4)]
  230. assert self.db.get_range(
  231. TSDBModel.project,
  232. [self.proj1.id],
  233. dts[0],
  234. dts[-1],
  235. rollup=3600,
  236. tenant_ids={"referrer": "r", "organization_id": 1234},
  237. ) == {
  238. self.proj1.id: [
  239. (timestamp(dts[0]), 6),
  240. (timestamp(dts[1]), 6),
  241. (timestamp(dts[2]), 6),
  242. (timestamp(dts[3]), 6),
  243. ]
  244. }
  245. def test_range_environment_filter(self):
  246. dts = [self.now + timedelta(hours=i) for i in range(4)]
  247. assert self.db.get_range(
  248. TSDBModel.project,
  249. [self.proj1.id],
  250. dts[0],
  251. dts[-1],
  252. rollup=3600,
  253. environment_ids=[self.env1.id],
  254. tenant_ids={"referrer": "r", "organization_id": 1234},
  255. ) == {
  256. self.proj1.id: [
  257. (timestamp(dts[0]), 6),
  258. (timestamp(dts[1]), 6),
  259. (timestamp(dts[2]), 0),
  260. (timestamp(dts[3]), 0),
  261. ]
  262. }
  263. # No events submitted for env2
  264. assert self.db.get_range(
  265. TSDBModel.project,
  266. [self.proj1.id],
  267. dts[0],
  268. dts[-1],
  269. rollup=3600,
  270. environment_ids=[self.env2.id],
  271. tenant_ids={"referrer": "r", "organization_id": 1234},
  272. ) == {
  273. self.proj1.id: [
  274. (timestamp(dts[0]), 0),
  275. (timestamp(dts[1]), 0),
  276. (timestamp(dts[2]), 0),
  277. (timestamp(dts[3]), 0),
  278. ]
  279. }
  280. # Events submitted with no environment should match default environment
  281. assert self.db.get_range(
  282. TSDBModel.project,
  283. [self.proj1.id],
  284. dts[0],
  285. dts[-1],
  286. rollup=3600,
  287. environment_ids=[self.defaultenv.id],
  288. tenant_ids={"referrer": "r", "organization_id": 1234},
  289. ) == {
  290. self.proj1.id: [
  291. (timestamp(dts[0]), 0),
  292. (timestamp(dts[1]), 0),
  293. (timestamp(dts[2]), 6),
  294. (timestamp(dts[3]), 6),
  295. ]
  296. }
  297. def test_range_rollups(self):
  298. # Daily
  299. daystart = self.now.replace(hour=0) # day buckets start on day boundaries
  300. dts = [daystart + timedelta(days=i) for i in range(2)]
  301. assert self.db.get_range(
  302. TSDBModel.project,
  303. [self.proj1.id],
  304. dts[0],
  305. dts[-1],
  306. rollup=86400,
  307. tenant_ids={"referrer": "r", "organization_id": 1234},
  308. ) == {self.proj1.id: [(timestamp(dts[0]), 24), (timestamp(dts[1]), 0)]}
  309. # Minutely
  310. dts = [self.now + timedelta(minutes=i) for i in range(120)]
  311. # Expect every 10th minute to have a 1, else 0
  312. expected = [(d.timestamp(), 1 if i % 10 == 0 else 0) for i, d in enumerate(dts)]
  313. assert self.db.get_range(
  314. TSDBModel.project,
  315. [self.proj1.id],
  316. dts[0],
  317. dts[-1],
  318. rollup=60,
  319. tenant_ids={"referrer": "r", "organization_id": 1234},
  320. ) == {self.proj1.id: expected}
  321. def test_distinct_counts_series_users(self):
  322. dts = [self.now + timedelta(hours=i) for i in range(4)]
  323. assert self.db.get_distinct_counts_series(
  324. TSDBModel.users_affected_by_group,
  325. [self.proj1group1.id],
  326. dts[0],
  327. dts[-1],
  328. rollup=3600,
  329. tenant_ids={"referrer": "r", "organization_id": 1234},
  330. ) == {
  331. self.proj1group1.id: [
  332. (timestamp(dts[0]), 1),
  333. (timestamp(dts[1]), 1),
  334. (timestamp(dts[2]), 1),
  335. (timestamp(dts[3]), 2),
  336. ]
  337. }
  338. dts = [self.now + timedelta(hours=i) for i in range(4)]
  339. assert self.db.get_distinct_counts_series(
  340. TSDBModel.users_affected_by_project,
  341. [self.proj1.id],
  342. dts[0],
  343. dts[-1],
  344. rollup=3600,
  345. tenant_ids={"referrer": "r", "organization_id": 1234},
  346. ) == {
  347. self.proj1.id: [
  348. (timestamp(dts[0]), 1),
  349. (timestamp(dts[1]), 2),
  350. (timestamp(dts[2]), 2),
  351. (timestamp(dts[3]), 2),
  352. ]
  353. }
  354. assert (
  355. self.db.get_distinct_counts_series(
  356. TSDBModel.users_affected_by_group,
  357. [],
  358. dts[0],
  359. dts[-1],
  360. rollup=3600,
  361. tenant_ids={"referrer": "r", "organization_id": 1234},
  362. )
  363. == {}
  364. )
  365. def test_get_distinct_counts_totals_users(self):
  366. assert self.db.get_distinct_counts_totals(
  367. TSDBModel.users_affected_by_group,
  368. [self.proj1group1.id],
  369. self.now,
  370. self.now + timedelta(hours=4),
  371. rollup=3600,
  372. tenant_ids={"referrer": "r", "organization_id": 1234},
  373. ) == {
  374. self.proj1group1.id: 5 # 5 unique users overall
  375. }
  376. assert self.db.get_distinct_counts_totals(
  377. TSDBModel.users_affected_by_group,
  378. [self.proj1group1.id],
  379. self.now,
  380. self.now,
  381. rollup=3600,
  382. tenant_ids={"referrer": "r", "organization_id": 1234},
  383. ) == {
  384. self.proj1group1.id: 1 # Only 1 unique user in the first hour
  385. }
  386. assert self.db.get_distinct_counts_totals(
  387. TSDBModel.users_affected_by_project,
  388. [self.proj1.id],
  389. self.now,
  390. self.now + timedelta(hours=4),
  391. rollup=3600,
  392. tenant_ids={"referrer": "r", "organization_id": 1234},
  393. ) == {self.proj1.id: 5}
  394. assert (
  395. self.db.get_distinct_counts_totals(
  396. TSDBModel.users_affected_by_group,
  397. [],
  398. self.now,
  399. self.now + timedelta(hours=4),
  400. rollup=3600,
  401. tenant_ids={"referrer": "r", "organization_id": 1234},
  402. )
  403. == {}
  404. )
  405. def test_get_distinct_counts_totals_users_with_conditions(self):
  406. assert self.db.get_distinct_counts_totals_with_conditions(
  407. TSDBModel.users_affected_by_group,
  408. [self.proj1group1.id],
  409. self.now,
  410. self.now + timedelta(hours=4),
  411. rollup=3600,
  412. tenant_ids={"referrer": "r", "organization_id": 1234},
  413. conditions=[("tags[region]", "=", "US")],
  414. ) == {
  415. self.proj1group1.id: 2 # 5 unique users with US tag
  416. }
  417. assert self.db.get_distinct_counts_totals_with_conditions(
  418. TSDBModel.users_affected_by_group,
  419. [self.proj1group1.id],
  420. self.now,
  421. self.now + timedelta(hours=4),
  422. rollup=3600,
  423. tenant_ids={"referrer": "r", "organization_id": 1234},
  424. conditions=[("tags[region]", "=", "EU")],
  425. ) == {
  426. self.proj1group1.id: 3 # 3 unique users with EU tag
  427. }
  428. assert self.db.get_distinct_counts_totals_with_conditions(
  429. TSDBModel.users_affected_by_group,
  430. [self.proj1group1.id],
  431. self.now,
  432. self.now + timedelta(hours=4),
  433. rollup=3600,
  434. tenant_ids={"referrer": "r", "organization_id": 1234},
  435. conditions=[("tags[region]", "=", "MARS")],
  436. ) == {self.proj1group1.id: 0}
  437. assert (
  438. self.db.get_distinct_counts_totals_with_conditions(
  439. TSDBModel.users_affected_by_group,
  440. [],
  441. self.now,
  442. self.now + timedelta(hours=4),
  443. rollup=3600,
  444. tenant_ids={"referrer": "r", "organization_id": 1234},
  445. )
  446. == {}
  447. )
  448. def test_most_frequent(self):
  449. assert self.db.get_most_frequent(
  450. TSDBModel.frequent_issues_by_project,
  451. [self.proj1.id],
  452. self.now,
  453. self.now + timedelta(hours=4),
  454. rollup=3600,
  455. tenant_ids={"referrer": "r", "organization_id": 1234},
  456. ) in [
  457. {self.proj1.id: [(self.proj1group1.id, 2.0), (self.proj1group2.id, 1.0)]},
  458. {self.proj1.id: [(self.proj1group2.id, 2.0), (self.proj1group1.id, 1.0)]},
  459. ] # Both issues equally frequent
  460. assert (
  461. self.db.get_most_frequent(
  462. TSDBModel.frequent_issues_by_project,
  463. [],
  464. self.now,
  465. self.now + timedelta(hours=4),
  466. rollup=3600,
  467. tenant_ids={"referrer": "r", "organization_id": 1234},
  468. )
  469. == {}
  470. )
  471. def test_frequency_series(self):
  472. dts = [self.now + timedelta(hours=i) for i in range(4)]
  473. assert self.db.get_frequency_series(
  474. TSDBModel.frequent_releases_by_group,
  475. {
  476. self.proj1group1.id: (self.group1release1env1.id, self.group1release2env1.id),
  477. self.proj1group2.id: (self.group2release1env1.id,),
  478. },
  479. dts[0],
  480. dts[-1],
  481. rollup=3600,
  482. tenant_ids={"referrer": "r", "organization_id": 1234},
  483. ) == {
  484. self.proj1group1.id: [
  485. (timestamp(dts[0]), {self.group1release1env1.id: 0, self.group1release2env1.id: 0}),
  486. (timestamp(dts[1]), {self.group1release1env1.id: 3, self.group1release2env1.id: 0}),
  487. (timestamp(dts[2]), {self.group1release1env1.id: 0, self.group1release2env1.id: 3}),
  488. (timestamp(dts[3]), {self.group1release1env1.id: 0, self.group1release2env1.id: 0}),
  489. ],
  490. self.proj1group2.id: [
  491. (timestamp(dts[0]), {self.group2release1env1.id: 0}),
  492. (timestamp(dts[1]), {self.group2release1env1.id: 3}),
  493. (timestamp(dts[2]), {self.group2release1env1.id: 0}),
  494. (timestamp(dts[3]), {self.group2release1env1.id: 0}),
  495. ],
  496. }
  497. assert (
  498. self.db.get_frequency_series(
  499. TSDBModel.frequent_releases_by_group,
  500. {},
  501. dts[0],
  502. dts[-1],
  503. rollup=3600,
  504. tenant_ids={"referrer": "r", "organization_id": 1234},
  505. )
  506. == {}
  507. )
  508. def test_result_shape(self):
  509. """
  510. Tests that the results from the different TSDB methods have the
  511. expected format.
  512. """
  513. project_id = self.proj1.id
  514. dts = [self.now + timedelta(hours=i) for i in range(4)]
  515. results = self.db.get_most_frequent(
  516. TSDBModel.frequent_issues_by_project,
  517. [project_id],
  518. dts[0],
  519. dts[0],
  520. tenant_ids={"referrer": "r", "organization_id": 1234},
  521. )
  522. assert has_shape(results, {1: [(1, 1.0)]})
  523. results = self.db.get_most_frequent_series(
  524. TSDBModel.frequent_issues_by_project,
  525. [project_id],
  526. dts[0],
  527. dts[0],
  528. tenant_ids={"referrer": "r", "organization_id": 1234},
  529. )
  530. assert has_shape(results, {1: [(1, {1: 1.0})]})
  531. items = {
  532. # {project_id: (issue_id, issue_id, ...)}
  533. project_id: (self.proj1group1.id, self.proj1group2.id)
  534. }
  535. results = self.db.get_frequency_series(
  536. TSDBModel.frequent_issues_by_project,
  537. items,
  538. dts[0],
  539. dts[-1],
  540. tenant_ids={"referrer": "r", "organization_id": 1234},
  541. )
  542. assert has_shape(results, {1: [(1, {1: 1})]})
  543. results = self.db.get_frequency_totals(
  544. TSDBModel.frequent_issues_by_project,
  545. items,
  546. dts[0],
  547. dts[-1],
  548. tenant_ids={"referrer": "r", "organization_id": 1234},
  549. )
  550. assert has_shape(results, {1: {1: 1}})
  551. results = self.db.get_range(
  552. TSDBModel.project,
  553. [project_id],
  554. dts[0],
  555. dts[-1],
  556. tenant_ids={"referrer": "r", "organization_id": 1234},
  557. )
  558. assert has_shape(results, {1: [(1, 1)]})
  559. results = self.db.get_distinct_counts_series(
  560. TSDBModel.users_affected_by_project,
  561. [project_id],
  562. dts[0],
  563. dts[-1],
  564. tenant_ids={"referrer": "r", "organization_id": 1234},
  565. )
  566. assert has_shape(results, {1: [(1, 1)]})
  567. results = self.db.get_distinct_counts_totals(
  568. TSDBModel.users_affected_by_project,
  569. [project_id],
  570. dts[0],
  571. dts[-1],
  572. tenant_ids={"referrer": "r", "organization_id": 1234},
  573. )
  574. assert has_shape(results, {1: 1})
  575. results = self.db.get_distinct_counts_union(
  576. TSDBModel.users_affected_by_project,
  577. [project_id],
  578. dts[0],
  579. dts[-1],
  580. tenant_ids={"referrer": "r", "organization_id": 1234},
  581. )
  582. assert has_shape(results, 1)
  583. def test_calculated_limit(self):
  584. with patch("sentry.tsdb.snuba.raw_snql_query") as snuba:
  585. # 24h test
  586. rollup = 3600
  587. end = self.now
  588. start = end + timedelta(days=-1, seconds=rollup)
  589. self.db.get_data(TSDBModel.group, [1, 2, 3, 4, 5], start, end, rollup=rollup)
  590. assert snuba.call_args.args[0].query.limit == Limit(120)
  591. # 14 day test
  592. rollup = 86400
  593. start = end + timedelta(days=-14, seconds=rollup)
  594. self.db.get_data(TSDBModel.group, [1, 2, 3, 4, 5], start, end, rollup=rollup)
  595. assert snuba.call_args.args[0].query.limit == Limit(70)
  596. # 1h test
  597. rollup = 3600
  598. end = self.now
  599. start = end + timedelta(hours=-1, seconds=rollup)
  600. self.db.get_data(TSDBModel.group, [1, 2, 3, 4, 5], start, end, rollup=rollup)
  601. assert snuba.call_args.args[0].query.limit == Limit(5)
  602. @patch("sentry.utils.snuba.OVERRIDE_OPTIONS", new={"consistent": True})
  603. def test_tsdb_with_consistent(self):
  604. with patch("sentry.utils.snuba._apply_cache_and_build_results") as snuba:
  605. rollup = 3600
  606. end = self.now
  607. start = end + timedelta(days=-1, seconds=rollup)
  608. self.db.get_data(TSDBModel.group, [1, 2, 3, 4, 5], start, end, rollup=rollup)
  609. assert snuba.call_args.args[0][0].request.query.limit == Limit(120)
  610. assert snuba.call_args.args[0][0].request.flags.consistent is True
  611. class SnubaTSDBGroupProfilingTest(TestCase, SnubaTestCase, SearchIssueTestMixin):
  612. def setUp(self):
  613. super().setUp()
  614. self.db = SnubaTSDB()
  615. self.now = before_now(hours=4).replace(hour=0, minute=0, second=0, microsecond=0)
  616. self.proj1 = self.create_project()
  617. self.env1 = Environment.objects.get_or_create(
  618. organization_id=self.proj1.organization_id, name="test"
  619. )[0]
  620. self.env2 = Environment.objects.get_or_create(
  621. organization_id=self.proj1.organization_id, name="dev"
  622. )[0]
  623. defaultenv = ""
  624. group1_fingerprint = f"{ProfileFileIOGroupType.type_id}-group1"
  625. group2_fingerprint = f"{ProfileFileIOGroupType.type_id}-group2"
  626. groups = {}
  627. for r in range(0, 14400, 600): # Every 10 min for 4 hours
  628. event, occurrence, group_info = self.store_search_issue(
  629. project_id=self.proj1.id,
  630. # change every 55 min so some hours have 1 user, some have 2
  631. user_id=r // 3300,
  632. fingerprints=[group1_fingerprint] if ((r // 600) % 2) else [group2_fingerprint],
  633. # release_version=str(r // 3600) * 10, # 1 per hour,
  634. environment=[self.env1.name, None][(r // 7200) % 3],
  635. insert_time=self.now + timedelta(seconds=r),
  636. )
  637. if group_info:
  638. groups[group_info.group.id] = group_info.group
  639. all_groups = list(groups.values())
  640. self.proj1group1 = all_groups[0]
  641. self.proj1group2 = all_groups[1]
  642. self.defaultenv = Environment.objects.get(name=defaultenv)
  643. def test_range_group_manual_group_time_rollup(self):
  644. project = self.create_project()
  645. # these are the only granularities/rollups that be actually be used
  646. GRANULARITIES = [
  647. (10, timedelta(seconds=10), 5),
  648. (60 * 60, timedelta(hours=1), 6),
  649. (60 * 60 * 24, timedelta(days=1), 15),
  650. ]
  651. start = before_now(days=15).replace(hour=0, minute=0, second=0)
  652. for step, delta, times in GRANULARITIES:
  653. series = [start + (delta * i) for i in range(times)]
  654. series_ts = [int(ts.timestamp()) for ts in series]
  655. assert self.db.get_optimal_rollup(series[0], series[-1]) == step
  656. assert self.db.get_optimal_rollup_series(series[0], end=series[-1], rollup=None) == (
  657. step,
  658. series_ts,
  659. )
  660. for time_step in series:
  661. _, _, group_info = self.store_search_issue(
  662. project_id=project.id,
  663. user_id=0,
  664. fingerprints=[f"test_range_group_manual_group_time_rollup-{step}"],
  665. environment=None,
  666. insert_time=time_step,
  667. )
  668. assert group_info is not None
  669. assert self.db.get_range(
  670. TSDBModel.group_generic,
  671. [group_info.group.id],
  672. series[0],
  673. series[-1],
  674. rollup=None,
  675. tenant_ids={"referrer": "test", "organization_id": 1},
  676. ) == {group_info.group.id: [(ts, 1) for ts in series_ts]}
  677. def test_range_groups_mult(self):
  678. now = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  679. dts = [now + timedelta(hours=i) for i in range(4)]
  680. project = self.create_project()
  681. group_fingerprint = f"{ProfileFileIOGroupType.type_id}-group4"
  682. groups = []
  683. for i in range(0, 11):
  684. _, _, group_info = self.store_search_issue(
  685. project_id=project.id,
  686. user_id=0,
  687. fingerprints=[group_fingerprint],
  688. environment=None,
  689. insert_time=now + timedelta(minutes=i * 10),
  690. )
  691. if group_info:
  692. groups.append(group_info.group)
  693. group = groups[0]
  694. assert self.db.get_range(
  695. TSDBModel.group_generic,
  696. [group.id],
  697. dts[0],
  698. dts[-1],
  699. rollup=3600,
  700. tenant_ids={"referrer": "test", "organization_id": 1},
  701. ) == {
  702. group.id: [
  703. (timestamp(dts[0]), 6),
  704. (timestamp(dts[1]), 5),
  705. (timestamp(dts[2]), 0),
  706. (timestamp(dts[3]), 0),
  707. ]
  708. }
  709. def test_range_groups_simple(self):
  710. project = self.create_project()
  711. now = before_now(days=1).replace(hour=10, minute=0, second=0, microsecond=0)
  712. group_fingerprint = f"{ProfileFileIOGroupType.type_id}-group5"
  713. ids = [1, 2, 3, 4, 5]
  714. groups = []
  715. for r in ids:
  716. # for r in range(0, 9, 1):
  717. event, occurrence, group_info = self.store_search_issue(
  718. project_id=project.id,
  719. # change every 55 min so some hours have 1 user, some have 2
  720. user_id=r,
  721. fingerprints=[group_fingerprint],
  722. environment=None,
  723. # release_version=str(r // 3600) * 10, # 1 per hour,
  724. insert_time=now,
  725. )
  726. if group_info:
  727. groups.append(group_info.group)
  728. group = groups[0]
  729. dts = [now + timedelta(hours=i) for i in range(4)]
  730. assert self.db.get_range(
  731. TSDBModel.group_generic,
  732. [group.id],
  733. dts[0],
  734. dts[-1],
  735. rollup=3600,
  736. tenant_ids={"referrer": "test", "organization_id": 1},
  737. ) == {
  738. group.id: [
  739. (timestamp(dts[0]), len(ids)),
  740. (timestamp(dts[1]), 0),
  741. (timestamp(dts[2]), 0),
  742. (timestamp(dts[3]), 0),
  743. ]
  744. }
  745. def test_range_groups(self):
  746. dts = [self.now + timedelta(hours=i) for i in range(4)]
  747. # Multiple groups
  748. assert self.db.get_range(
  749. TSDBModel.group_generic,
  750. [self.proj1group1.id, self.proj1group2.id],
  751. dts[0],
  752. dts[-1],
  753. rollup=3600,
  754. tenant_ids={"referrer": "test", "organization_id": 1},
  755. ) == {
  756. self.proj1group1.id: [
  757. (timestamp(dts[0]), 3),
  758. (timestamp(dts[1]), 3),
  759. (timestamp(dts[2]), 3),
  760. (timestamp(dts[3]), 3),
  761. ],
  762. self.proj1group2.id: [
  763. (timestamp(dts[0]), 3),
  764. (timestamp(dts[1]), 3),
  765. (timestamp(dts[2]), 3),
  766. (timestamp(dts[3]), 3),
  767. ],
  768. }
  769. assert (
  770. self.db.get_range(
  771. TSDBModel.group_generic,
  772. [],
  773. dts[0],
  774. dts[-1],
  775. rollup=3600,
  776. tenant_ids={"referrer": "test", "organization_id": 1},
  777. )
  778. == {}
  779. )
  780. def test_get_distinct_counts_totals_users(self):
  781. assert self.db.get_distinct_counts_totals(
  782. TSDBModel.users_affected_by_generic_group,
  783. [self.proj1group1.id],
  784. self.now,
  785. self.now + timedelta(hours=4),
  786. rollup=3600,
  787. tenant_ids={"referrer": "test", "organization_id": 1},
  788. ) == {
  789. self.proj1group1.id: 5 # 5 unique users overall
  790. }
  791. assert self.db.get_distinct_counts_totals(
  792. TSDBModel.users_affected_by_generic_group,
  793. [self.proj1group1.id],
  794. self.now,
  795. self.now,
  796. rollup=3600,
  797. tenant_ids={"referrer": "test", "organization_id": 1},
  798. ) == {
  799. self.proj1group1.id: 1 # Only 1 unique user in the first hour
  800. }
  801. assert (
  802. self.db.get_distinct_counts_totals(
  803. TSDBModel.users_affected_by_generic_group,
  804. [],
  805. self.now,
  806. self.now + timedelta(hours=4),
  807. rollup=3600,
  808. tenant_ids={"referrer": "test", "organization_id": 1},
  809. )
  810. == {}
  811. )
  812. def test_get_sums(self):
  813. assert self.db.get_sums(
  814. model=TSDBModel.group_generic,
  815. keys=[self.proj1group1.id, self.proj1group2.id],
  816. start=self.now,
  817. end=self.now + timedelta(hours=4),
  818. tenant_ids={"referrer": "test", "organization_id": 1},
  819. ) == {self.proj1group1.id: 12, self.proj1group2.id: 12}
  820. def test_get_data_or_conditions_parsed(self):
  821. """
  822. Verify parsing the legacy format with nested OR conditions works
  823. """
  824. conditions = [
  825. # or conditions in the legacy format needs open and close brackets for precedence
  826. # there's some special casing when parsing conditions that specifically handles this
  827. [
  828. [["isNull", ["environment"]], "=", 1],
  829. ["environment", "IN", [self.env1.name]],
  830. ]
  831. ]
  832. data1 = self.db.get_data(
  833. model=TSDBModel.group_generic,
  834. keys=[self.proj1group1.id, self.proj1group2.id],
  835. conditions=conditions,
  836. start=self.now,
  837. end=self.now + timedelta(hours=4),
  838. tenant_ids={"referrer": "test", "organization_id": 1},
  839. )
  840. data2 = self.db.get_data(
  841. model=TSDBModel.group_generic,
  842. keys=[self.proj1group1.id, self.proj1group2.id],
  843. start=self.now,
  844. end=self.now + timedelta(hours=4),
  845. tenant_ids={"referrer": "test", "organization_id": 1},
  846. )
  847. # the above queries should return the same data since all groups either have:
  848. # environment=None or environment=test
  849. # so the condition really shouldn't be filtering anything
  850. assert data1 == data2
  851. class AddJitterToSeriesTest(TestCase):
  852. def setUp(self):
  853. self.db = SnubaTSDB()
  854. def run_test(self, end, interval, jitter, expected_start, expected_end):
  855. start = end - interval
  856. rollup, rollup_series = self.db.get_optimal_rollup_series(start, end)
  857. series = self.db._add_jitter_to_series(rollup_series, start, rollup, jitter)
  858. assert to_datetime(series[0]) == expected_start
  859. assert to_datetime(series[-1]) == expected_end
  860. def test(self):
  861. self.run_test(
  862. end=datetime(2022, 5, 18, 10, 23, 4, tzinfo=UTC),
  863. interval=timedelta(hours=1),
  864. jitter=5,
  865. expected_start=datetime(2022, 5, 18, 9, 22, 55, tzinfo=UTC),
  866. expected_end=datetime(2022, 5, 18, 10, 22, 55, tzinfo=UTC),
  867. )
  868. self.run_test(
  869. end=datetime(2022, 5, 18, 10, 23, 8, tzinfo=UTC),
  870. interval=timedelta(hours=1),
  871. jitter=5,
  872. expected_start=datetime(2022, 5, 18, 9, 23, 5, tzinfo=UTC),
  873. expected_end=datetime(2022, 5, 18, 10, 23, 5, tzinfo=UTC),
  874. )
  875. # Jitter should be the same
  876. self.run_test(
  877. end=datetime(2022, 5, 18, 10, 23, 8, tzinfo=UTC),
  878. interval=timedelta(hours=1),
  879. jitter=55,
  880. expected_start=datetime(2022, 5, 18, 9, 23, 5, tzinfo=UTC),
  881. expected_end=datetime(2022, 5, 18, 10, 23, 5, tzinfo=UTC),
  882. )
  883. self.run_test(
  884. end=datetime(2022, 5, 18, 22, 33, 2, tzinfo=UTC),
  885. interval=timedelta(minutes=1),
  886. jitter=3,
  887. expected_start=datetime(2022, 5, 18, 22, 31, 53, tzinfo=UTC),
  888. expected_end=datetime(2022, 5, 18, 22, 32, 53, tzinfo=UTC),
  889. )
  890. def test_empty_series(self):
  891. assert self.db._add_jitter_to_series([], datetime(2022, 5, 18, 10, 23, 4), 60, 127) == []
  892. assert self.db._add_jitter_to_series([], datetime(2022, 5, 18, 10, 23, 4), 60, None) == []