From deae600a3d95b65422ec6f348c4d6d6f0725fdef Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Mon, 13 May 2024 12:48:38 -0400 Subject: [PATCH 01/31] feat(sdk): Bump SDK to 8.0.0 (#70772) --- package.json | 12 ++-- yarn.lock | 178 +++++++++++++++++++++++++-------------------------- 2 files changed, 95 insertions(+), 95 deletions(-) diff --git a/package.json b/package.json index dd46b7d713b072..4f14d5bf158339 100644 --- a/package.json +++ b/package.json @@ -57,13 +57,13 @@ "@sentry-internal/rrweb-player": "2.12.0", "@sentry-internal/rrweb-snapshot": "2.12.0", "@sentry/babel-plugin-component-annotate": "^2.16.1", - "@sentry/core": "^8.0.0-rc.2", - "@sentry/node": "^8.0.0-rc.2", - "@sentry/react": "^8.0.0-rc.2", + "@sentry/core": "^8.0.0", + "@sentry/node": "^8.0.0", + "@sentry/react": "^8.0.0", "@sentry/release-parser": "^1.3.1", "@sentry/status-page-list": "^0.1.0", - "@sentry/types": "^8.0.0-rc.2", - "@sentry/utils": "^8.0.0-rc.2", + "@sentry/types": "^8.0.0", + "@sentry/utils": "^8.0.0", "@spotlightjs/spotlight": "^2.0.0-alpha.1", "@tanstack/react-query": "^4.29.7", "@tanstack/react-query-devtools": "^4.36.1", @@ -178,7 +178,7 @@ "@codecov/webpack-plugin": "^0.0.1-beta.6", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@sentry/jest-environment": "^4.0.0", - "@sentry/profiling-node": "^8.0.0-rc.2", + "@sentry/profiling-node": "^8.0.0", "@styled/typescript-styled-plugin": "^1.0.1", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", diff --git a/yarn.lock b/yarn.lock index a943cbf82d241a..b6aa815672cd90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3004,23 +3004,23 @@ fs-extra "^11.1.1" lodash "^4.17.21" -"@sentry-internal/browser-utils@8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.0.0-rc.2.tgz#1e938eef8bb371343a87c49c5185d250fb7b0fd9" - integrity sha512-QwN4OHPoaXK6FShj7jNUXQIsm9/mqxcqFK2jAdTZZHhucyOYSgpu17Mb+LVYvncBF+4O2MXrt5oa8KwTUKohRw== +"@sentry-internal/browser-utils@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.0.0.tgz#69bd216abf78d9eef2e6fcdf99d7c281986508d7" + integrity sha512-dzmoDK7mzP7MvNt/jjs9a4OQ18H/8NyhDiKoJakVZnvk8ComGIv01vOOxDhrNmLyaJSq2KVNsiIJ+AkTmwcmyQ== dependencies: - "@sentry/core" "8.0.0-rc.2" - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" + "@sentry/core" "8.0.0" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" -"@sentry-internal/feedback@8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.0.0-rc.2.tgz#123fb2db78da44833bf4688708c5082c86541820" - integrity sha512-VEnlWZ2hDQBdr5fbPbljdfFvboFHTAfU6fjth5HaAxBwf+BXMTnNuZ0qBFa68MKOy+IKt4r+a/LwbMW8faVicA== +"@sentry-internal/feedback@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.0.0.tgz#94795aea2d7fb23e698e50032899e9b4f4d04a68" + integrity sha512-2Jj0B5xn1y5kiOwso7EWQDlLGRt1tGcnglIYqCIpwNQM38yqn+5NMwH/Df7TkBlxBerKo4MYZZ2yHNUpkTXQ7Q== dependencies: - "@sentry/core" "8.0.0-rc.2" - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" + "@sentry/core" "8.0.0" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" "@sentry-internal/global-search@^1.0.0": version "1.0.0" @@ -3041,25 +3041,25 @@ resolved "https://registry.yarnpkg.com/@sentry-internal/react-inspector/-/react-inspector-6.0.1-4.tgz#10758f3461cf2cf48df8c80f0514c55ca18872c5" integrity sha512-uL2RyvW8EqDEchnbo8Hu/c4IpBqM3LLxUpZPHs8o40kynerzPset6bC/m5SU124gEhy4PqjdvJ7DhTYR75NetQ== -"@sentry-internal/replay-canvas@8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.0.0-rc.2.tgz#06a3d0d6486cb93dffb9283b5ac3e3220d7dd81c" - integrity sha512-27HKSeZNR9ZI3PcOLQFvPVl25fi7YNluWl1o9VRRXV96ug7bjX6+4n3m8z17r6kZAKP4jDV8YtAnzAjuz0MhOQ== +"@sentry-internal/replay-canvas@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.0.0.tgz#eb4da3ec5c8371ed9112ad69712d960c24ee28b5" + integrity sha512-jeE5YQ42groVRvbM41iL4rxvWuKOVnZ7UXacHDgWerR2S+C7OtN3Ydzr34rfRYTVagqFPDcDswFrxrcWuZD+Kw== dependencies: - "@sentry-internal/replay" "8.0.0-rc.2" - "@sentry/core" "8.0.0-rc.2" - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" + "@sentry-internal/replay" "8.0.0" + "@sentry/core" "8.0.0" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" -"@sentry-internal/replay@8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.0.0-rc.2.tgz#6e194883ff787ad3b6f6cfbd06e70191b570d82f" - integrity sha512-Xn3XYETGcQECwN/hkcSK9OPg7ch4yekVQ6C2hnsRFA2qxHc5cOXw6jGvYUyzKMw6VMKOzO+ONJAzc+kpvrB+8Q== +"@sentry-internal/replay@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.0.0.tgz#3ba2dea8df79d1e31e4ec0ce948dfbb994731b0f" + integrity sha512-lh0opJuhvKFgLK0TxeN2FDhnCc9qNdgBOpjA69hwpKl10kMxDoZy+oLxE4hx8j5RYKtM2o7mCv2rf1n0wK22Kg== dependencies: - "@sentry-internal/browser-utils" "8.0.0-rc.2" - "@sentry/core" "8.0.0-rc.2" - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" + "@sentry-internal/browser-utils" "8.0.0" + "@sentry/core" "8.0.0" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" "@sentry-internal/rrdom@2.12.0": version "2.12.0" @@ -3107,36 +3107,36 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.16.1.tgz#da3bf4ec1c1dc68a97d6a7e27bd710001d6b07fb" integrity sha512-pJka66URsqQbk6hTs9H1XFpUeI0xxuqLYf9Dy5pRGNHSJMtfv91U+CaYSWt03aRRMGDXMduh62zAAY7Wf0HO+A== -"@sentry/browser@8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.0.0-rc.2.tgz#c8a3f93a94e5adbab9859f67c60ad8054c9c9b51" - integrity sha512-FpsW4JiU47QEP3pe6huC0n98GYfJxcaLHKV+MYKi12XS7ca6bftOJPVv0BmHvBa9xrEBKbVsyCZIHZTNXrTI4w== - dependencies: - "@sentry-internal/browser-utils" "8.0.0-rc.2" - "@sentry-internal/feedback" "8.0.0-rc.2" - "@sentry-internal/replay" "8.0.0-rc.2" - "@sentry-internal/replay-canvas" "8.0.0-rc.2" - "@sentry/core" "8.0.0-rc.2" - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" - -"@sentry/core@8.0.0-rc.2", "@sentry/core@^8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.0.0-rc.2.tgz#839f447b263d69ec20264533e905a6db06835bc6" - integrity sha512-GNG0VYFS5EiJJHbJ9nRc3CPb2EU2eAtkDlWlQtkKu/jvHE7NG6ik8qk841Yw3ki7KWN05IVMD5FhtxDHjEYXkw== +"@sentry/browser@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.0.0.tgz#9c2f2d62f196a5b9b5a7174d0970c4f974722c98" + integrity sha512-HZt5bjujxz2XJA1iUqD51gEz/h8Ij+BYLu6D+qh6WpVtCSS1cfKoxJj8mQef7j5tIVVofxRtRr9PvAoFnehO0A== + dependencies: + "@sentry-internal/browser-utils" "8.0.0" + "@sentry-internal/feedback" "8.0.0" + "@sentry-internal/replay" "8.0.0" + "@sentry-internal/replay-canvas" "8.0.0" + "@sentry/core" "8.0.0" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" + +"@sentry/core@8.0.0", "@sentry/core@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.0.0.tgz#fd5f94c9ef72ce386ae37de852f156106ea807d5" + integrity sha512-PgOqQPdlIbiLFOo0F6IBzMbvusiEQ86+yXd76pIsuqQ2tj+iFL5gdYOckF/FKVpAwhfzIx64GKin2C+535c1qQ== dependencies: - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" "@sentry/jest-environment@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@sentry/jest-environment/-/jest-environment-4.0.0.tgz#037844bed70c8f13259ee01ab65ff8d36aef0209" integrity sha512-91jLBS8KbX2Ng0aDSP7kdE9sjiLc4qjp/jczTbmvOvuHxoaQ9hSLaEpsthnnUQ/zNeprZMkOC9xlS+zABw3Zmw== -"@sentry/node@8.0.0-rc.2", "@sentry/node@^8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.0.0-rc.2.tgz#d36bc14bd3348e5199aa1cd0bd7898bb27084c9a" - integrity sha512-CcoU2ANmEPG1I1E1a8G57bRmy+6etxNprnoe4b6L5+y7b5KVpJq2nnq1RjYiJwN3H5oG4h7sOrc8RIbqPox4Zw== +"@sentry/node@8.0.0", "@sentry/node@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-8.0.0.tgz#bf5bf7dd7810ede3cc7526a94990faeb545019b0" + integrity sha512-yOmJV0gyRA5KMw4lUAuB2LytUwcwSByjFn2KO5Xy9Oc8XpgJ91CIU/v1Udv3GsrYo2HpdQn/dyZLwqqhbyM55Q== dependencies: "@opentelemetry/api" "^1.8.0" "@opentelemetry/context-async-hooks" "^1.23.0" @@ -3160,43 +3160,43 @@ "@opentelemetry/sdk-trace-base" "^1.23.0" "@opentelemetry/semantic-conventions" "^1.23.0" "@prisma/instrumentation" "5.13.0" - "@sentry/core" "8.0.0-rc.2" - "@sentry/opentelemetry" "8.0.0-rc.2" - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" + "@sentry/core" "8.0.0" + "@sentry/opentelemetry" "8.0.0" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" optionalDependencies: opentelemetry-instrumentation-fetch-node "1.2.0" -"@sentry/opentelemetry@8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.0.0-rc.2.tgz#25a1dc55ec30204380fcc6f83afbe0378feabd3a" - integrity sha512-HW+LYzI90/ptu8pTkof/G2V9TOxu07p0vDwI4KayrjrLUVgi0q7JvhXviKghJRqBbLd1G9bOj3gD5J5yHvPDzg== +"@sentry/opentelemetry@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry/opentelemetry/-/opentelemetry-8.0.0.tgz#14a9a28144cd4c3da8d65a764aee20740033c03b" + integrity sha512-AvUUZpiJTq3H69Hd9k0tiOqGTA87uq0wZN+WaV4iT6sItG2MVaqYup5wSmqNKD6iVErfx7djzZ5C3LWsFQX3KQ== dependencies: - "@sentry/core" "8.0.0-rc.2" - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" + "@sentry/core" "8.0.0" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" -"@sentry/profiling-node@^8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry/profiling-node/-/profiling-node-8.0.0-rc.2.tgz#d5280a5c5497e3c1b1531c2507d7a3a87e8ae2e1" - integrity sha512-I3y9bv3SJdF5R9mM260BOgfyeTp2QZ+22W28lJXhg1hQbN8Bdf2eoWyxxgfo8fswf/uqLrlfKIjzugJ+BaNedw== +"@sentry/profiling-node@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry/profiling-node/-/profiling-node-8.0.0.tgz#d580ffd300047a85ebb80e959f2abe0f42576593" + integrity sha512-mhpErfHgYxlkxmlLlDzX8qvk12t8B0jyoS9oCIPe1m9Z+hQCI4BL/fEl01kIysMXaoKNaJ6ttXYoacLb+Xu+vA== dependencies: - "@sentry/core" "8.0.0-rc.2" - "@sentry/node" "8.0.0-rc.2" - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" + "@sentry/core" "8.0.0" + "@sentry/node" "8.0.0" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" detect-libc "^2.0.2" node-abi "^3.61.0" -"@sentry/react@^8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.0.0-rc.2.tgz#0ae48313a97467dcce437d72f3e20ff52e3e4416" - integrity sha512-6fniK1v2OxyUOy7qb0nLOtiY2xsk4VL9FFmqFvxLzSzyIe2RBSSfENtEmOg+xVvNSrnG96EOUhZMCsDdJote0g== +"@sentry/react@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.0.0.tgz#0b23c2abb93557ac623aadee90126f6c6a188e6a" + integrity sha512-TbAtOAKY2QKqpuy0uoO/ujL0J5djZjQ2K8iE3/j3+EX/3DaZ3ydUGNdJ0rDQQoJgRUsPidBM+SBco2dI38sCdQ== dependencies: - "@sentry/browser" "8.0.0-rc.2" - "@sentry/core" "8.0.0-rc.2" - "@sentry/types" "8.0.0-rc.2" - "@sentry/utils" "8.0.0-rc.2" + "@sentry/browser" "8.0.0" + "@sentry/core" "8.0.0" + "@sentry/types" "8.0.0" + "@sentry/utils" "8.0.0" hoist-non-react-statics "^3.3.2" "@sentry/release-parser@^1.3.1": @@ -3209,17 +3209,17 @@ resolved "https://registry.yarnpkg.com/@sentry/status-page-list/-/status-page-list-0.1.0.tgz#49e8683091de0531aba96fc95f19891970929701" integrity sha512-wXWu3IihxFO0l5WQkr6V138ZJKHpL8G7fw/9l0Dl6Nl1ggWcJZOaBN/o5sXasS1e0Atvy2dL9DiPsKmBq8D4MA== -"@sentry/types@8.0.0-rc.2", "@sentry/types@^8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.0.0-rc.2.tgz#e2ec9e29f1b0b0af2ce20b6ba6be45b606955707" - integrity sha512-A52WamMnmJnRFrw6S9tmp52eGSdRWlTTvbXMF5mBE/8RCwknAFuPcXetFpKtU/ixqK+oeGtXLrJOuSJhWDvnVg== +"@sentry/types@8.0.0", "@sentry/types@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.0.0.tgz#5047dcbcff6a38699e4490acd941baffafb72f45" + integrity sha512-Dtd8dtFEq1XtdAntkguYHaL4tokzm4Aq5t0HP6Vl1P+MPImokDE1UcpKglkh0Z5aym/vF6e0qW9/CM7lAI5R/A== -"@sentry/utils@8.0.0-rc.2", "@sentry/utils@^8.0.0-rc.2": - version "8.0.0-rc.2" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.0.0-rc.2.tgz#d915ed23a71d1a5b627f209380b1595eccbf81da" - integrity sha512-Ev0nhHVfMb82gtUHuqfbJNaeQZG/wzzO0+hiUFOXuGdJEFItYpv/z2TlfXEFQ8NX8nD0gWSFMlUCF8ySi0IMXA== +"@sentry/utils@8.0.0", "@sentry/utils@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.0.0.tgz#24b2f3f24cf521c0180e4335da63a4c6e51fa7dd" + integrity sha512-oZex/dRKfkWHoK99W7QDQtr26IZrAD9EDd2+pwLmkFclApxVDGLLKNkmcbfj4LX1zMROxKWww/GTE7eo08tEKg== dependencies: - "@sentry/types" "8.0.0-rc.2" + "@sentry/types" "8.0.0" "@sinclair/typebox@^0.27.8": version "0.27.8" From d3fd56ae673aabc4b6e1e9babd3d75680e7e6c85 Mon Sep 17 00:00:00 2001 From: Colleen O'Rourke Date: Mon, 13 May 2024 10:02:37 -0700 Subject: [PATCH 02/31] ref(delayed rules): Don't use a decorator, call add_handler instead (#70689) It doesn't seem like any of the delayed processing is being called since it's not imported by anything, so this PR changes the registry pattern a bit so that it does. --- src/sentry/buffer/redis.py | 9 ++++----- src/sentry/rules/processing/delayed_processing.py | 3 +-- src/sentry/tasks/post_process.py | 6 ++++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/sentry/buffer/redis.py b/src/sentry/buffer/redis.py index e1937ce9e59922..38a093561b4db3 100644 --- a/src/sentry/buffer/redis.py +++ b/src/sentry/buffer/redis.py @@ -63,12 +63,11 @@ class BufferHookRegistry: def __init__(self, *args: Any, **kwargs: Any) -> None: self._registry: dict[BufferHookEvent, Callable[..., Any]] = {} - def add_handler(self, key: BufferHookEvent) -> Callable[..., Any]: - def decorator(func: Callable[..., Any]) -> Callable[..., Any]: - self._registry[key] = func - return func + def add_handler(self, key: BufferHookEvent, func: Callable[..., Any]) -> None: + self._registry[key] = func - return decorator + def has(self, key: BufferHookEvent) -> bool: + return self._registry.get(key) is not None def callback(self, buffer_hook_event: BufferHookEvent, data: RedisBuffer) -> bool: try: diff --git a/src/sentry/rules/processing/delayed_processing.py b/src/sentry/rules/processing/delayed_processing.py index e018b117497937..4cabe5086fe34d 100644 --- a/src/sentry/rules/processing/delayed_processing.py +++ b/src/sentry/rules/processing/delayed_processing.py @@ -5,7 +5,7 @@ from typing import Any, DefaultDict, NamedTuple from sentry import nodestore -from sentry.buffer.redis import BufferHookEvent, RedisBuffer, redis_buffer_registry +from sentry.buffer.redis import RedisBuffer from sentry.eventstore.models import Event, GroupEvent from sentry.issues.issue_occurrence import IssueOccurrence from sentry.models.group import Group @@ -292,7 +292,6 @@ def get_group_to_groupevent( return group_to_groupevent -@redis_buffer_registry.add_handler(BufferHookEvent.FLUSH) def process_delayed_alert_conditions(buffer: RedisBuffer) -> None: with metrics.timer("delayed_processing.process_all_conditions.duration"): fetch_time = datetime.now(tz=timezone.utc) diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index aa05b4494ca032..5469e381a06369 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -990,6 +990,12 @@ def _get_replay_id(event): def process_rules(job: PostProcessJob) -> None: + from sentry.buffer.redis import BufferHookEvent, redis_buffer_registry + from sentry.rules.processing.delayed_processing import process_delayed_alert_conditions + + if not redis_buffer_registry.has(BufferHookEvent.FLUSH): + redis_buffer_registry.add_handler(BufferHookEvent.FLUSH, process_delayed_alert_conditions) + if job["is_reprocessed"]: return From da8fba1ff86e9df1a10d78222fdc41821d10e8f7 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 13 May 2024 13:22:24 -0400 Subject: [PATCH 03/31] fix(feedback): check latest event for attachment (#70778) we send screenshots through the feedback event, so instead of checking the issue for attachments, we should check the latest event. this should fix the issue where the `IconImage` isn't showing up when a feedback has a screenshot attached. SCR-20240513-jcau --- src/sentry/api/endpoints/group_details.py | 9 ++++++--- src/sentry/api/serializers/models/group_stream.py | 13 ++++++++----- .../endpoints/test_organization_group_index.py | 12 ++++++------ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/sentry/api/endpoints/group_details.py b/src/sentry/api/endpoints/group_details.py index e1332d62dbf0f8..1efa34de8cd93d 100644 --- a/src/sentry/api/endpoints/group_details.py +++ b/src/sentry/api/endpoints/group_details.py @@ -236,7 +236,7 @@ def get(self, request: Request, group) -> Response: ) data.update({"sentryAppIssues": sentry_app_issues}) - if "hasAttachments" in expand: + if "latestEventHasAttachments" in expand: if not features.has( "organizations:event-attachments", group.project.organization, @@ -244,8 +244,11 @@ def get(self, request: Request, group) -> Response: ): return self.respond(status=404) - num_attachments = EventAttachment.objects.filter(group_id=group.id).count() - data.update({"hasAttachments": num_attachments > 0}) + latest_event = group.get_latest_event() + num_attachments = EventAttachment.objects.filter( + project_id=latest_event.project_id, event_id=latest_event.event_id + ).count() + data.update({"latestEventHasAttachments": num_attachments > 0}) data.update( { diff --git a/src/sentry/api/serializers/models/group_stream.py b/src/sentry/api/serializers/models/group_stream.py index 93c770a64f6df9..c80ba5c2ed225d 100644 --- a/src/sentry/api/serializers/models/group_stream.py +++ b/src/sentry/api/serializers/models/group_stream.py @@ -397,7 +397,7 @@ def get_attrs( ) attrs[item].update({"sentryAppIssues": sentry_app_issues}) - if self._expand("hasAttachments"): + if self._expand("latestEventHasAttachments"): if not features.has( "organizations:event-attachments", item.project.organization, @@ -405,9 +405,12 @@ def get_attrs( ): return self.respond(status=404) + latest_event = item.get_latest_event() for item in item_list: - num_attachments = EventAttachment.objects.filter(group_id=item.id).count() - attrs[item].update({"hasAttachments": num_attachments > 0}) + num_attachments = EventAttachment.objects.filter( + project_id=latest_event.project_id, event_id=latest_event.event_id + ).count() + attrs[item].update({"latestEventHasAttachments": num_attachments > 0}) return attrs @@ -467,8 +470,8 @@ def serialize( if self._expand("sentryAppIssues"): result["sentryAppIssues"] = attrs["sentryAppIssues"] - if self._expand("hasAttachments"): - result["hasAttachments"] = attrs["hasAttachments"] + if self._expand("latestEventHasAttachments"): + result["latestEventHasAttachments"] = attrs["latestEventHasAttachments"] return result diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py index f9b6646b8d3903..16862652328f7d 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_index.py +++ b/tests/sentry/issues/endpoints/test_organization_group_index.py @@ -1862,7 +1862,7 @@ def test_expand_sentry_app_issues(self) -> None: assert response.data[0]["sentryAppIssues"][1]["displayName"] == issue_2.display_name @with_feature("organizations:event-attachments") - def test_expand_has_attachments(self) -> None: + def test_expand_latest_event_has_attachments(self) -> None: event = self.store_event( data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, project_id=self.project.id, @@ -1870,20 +1870,20 @@ def test_expand_has_attachments(self) -> None: query = "status:unresolved" self.login_as(user=self.user) response = self.get_response( - sort_by="date", limit=10, query=query, expand=["hasAttachments"] + sort_by="date", limit=10, query=query, expand=["latestEventHasAttachments"] ) assert response.status_code == 200 assert len(response.data) == 1 assert int(response.data[0]["id"]) == event.group.id # No attachments - assert response.data[0]["hasAttachments"] is False + assert response.data[0]["latestEventHasAttachments"] is False # Test with no expand response = self.get_response(sort_by="date", limit=10, query=query) assert response.status_code == 200 assert len(response.data) == 1 assert int(response.data[0]["id"]) == event.group.id - assert "hasAttachments" not in response.data[0] + assert "latestEventHasAttachments" not in response.data[0] # Add 1 attachment file_attachment = File.objects.create(name="hello.png", type="image/png") @@ -1897,10 +1897,10 @@ def test_expand_has_attachments(self) -> None: ) response = self.get_response( - sort_by="date", limit=10, query=query, expand=["hasAttachments"] + sort_by="date", limit=10, query=query, expand=["latestEventHasAttachments"] ) assert response.status_code == 200 - assert response.data[0]["hasAttachments"] is True + assert response.data[0]["latestEventHasAttachments"] is True def test_expand_owners(self) -> None: event = self.store_event( From 92d2852522398f5578990529872ec072fe751d97 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Mon, 13 May 2024 13:49:45 -0400 Subject: [PATCH 04/31] feat(crons): Remove culprit from crons incident issues (#70781) This is a bit confusing, we don't need a culprit --- src/sentry/monitors/logic/mark_failed.py | 2 +- tests/sentry/monitors/logic/test_mark_failed.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/monitors/logic/mark_failed.py b/src/sentry/monitors/logic/mark_failed.py index 73b20e3e69d4e1..083f721e196cbc 100644 --- a/src/sentry/monitors/logic/mark_failed.py +++ b/src/sentry/monitors/logic/mark_failed.py @@ -265,7 +265,7 @@ def create_issue_platform_occurrence( ), ], evidence_data={}, - culprit="incident", + culprit="", detection_time=current_timestamp, level="error", assignee=monitor_env.monitor.owner_actor, diff --git a/tests/sentry/monitors/logic/test_mark_failed.py b/tests/sentry/monitors/logic/test_mark_failed.py index 2d13f9069f5ecb..65fd5f00e47d71 100644 --- a/tests/sentry/monitors/logic/test_mark_failed.py +++ b/tests/sentry/monitors/logic/test_mark_failed.py @@ -296,7 +296,7 @@ def test_mark_failed_default_params_issue_platform(self, mock_produce_occurrence ], "type": MonitorIncidentType.type_id, "level": "error", - "culprit": "incident", + "culprit": "", }, ) == dict(occurrence) From ac80048be29f718d4caf7000034be7ac19f0e8f3 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Mon, 13 May 2024 13:50:03 -0400 Subject: [PATCH 05/31] ref(crons): Fix typing in testutils (#70774) --- src/sentry/monitors/testutils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sentry/monitors/testutils.py b/src/sentry/monitors/testutils.py index 316ae35fc6d3c5..2957781443f4b9 100644 --- a/src/sentry/monitors/testutils.py +++ b/src/sentry/monitors/testutils.py @@ -10,14 +10,19 @@ from sentry.monitors.types import CheckinItem, CheckinPayload -def build_checkin_item(ts=None, partition=0, message_overrides=None, payload_overrides=None): +def build_checkin_item( + ts: datetime | None = None, + partition: int = 0, + message_overrides=None, + payload_overrides=None, +): if ts is None: ts = datetime.now() message: CheckIn = { "message_type": "check_in", "payload": {}, - "start_time": ts, + "start_time": ts.timestamp(), "project_id": 1, "sdk": None, "retention_days": 10, From 72cf165bad13a009e373c6e59e8cf78ad1ca80c4 Mon Sep 17 00:00:00 2001 From: Nar Saynorath Date: Mon, 13 May 2024 14:29:10 -0400 Subject: [PATCH 06/31] feat(dashboards): Add feature flag for span metrics (#70788) We're going to surface more span metrics in the Dashboards product. Adding a feature flag to conditionally show these changes in the metrics product. --- src/sentry/features/temporary.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 30271366d96c9d..395cd93c605072 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -57,6 +57,7 @@ def register_temporary_features(manager: FeatureManager): manager.add("organizations:dashboards-import", OrganizationFeature, FeatureHandlerStrategy.REMOTE) manager.add("organizations:dashboards-mep", OrganizationFeature, FeatureHandlerStrategy.REMOTE) manager.add("organizations:dashboards-rh-widget", OrganizationFeature, FeatureHandlerStrategy.REMOTE) + manager.add("organizations:dashboards-span-metrics", OrganizationFeature, FeatureHandlerStrategy.OPTIONS) manager.add("organizations:ddm-dashboard-import", OrganizationFeature, FeatureHandlerStrategy.REMOTE) manager.add("organizations:custom-metrics-experimental", OrganizationFeature, FeatureHandlerStrategy.REMOTE) manager.add("organizations:ddm-sidebar-item-hidden", OrganizationFeature, FeatureHandlerStrategy.REMOTE) From 0fc316c974f88927566ca8f333a684cf238b6fda Mon Sep 17 00:00:00 2001 From: Enoch Tang Date: Mon, 13 May 2024 14:32:00 -0400 Subject: [PATCH 07/31] chore(snuba-sdk): Bump snuba sdk to 2.0.34 (#70589) --- requirements-dev-frozen.txt | 2 +- requirements-frozen.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev-frozen.txt b/requirements-dev-frozen.txt index 5676b0b44002fd..47270c6d094104 100644 --- a/requirements-dev-frozen.txt +++ b/requirements-dev-frozen.txt @@ -189,7 +189,7 @@ sentry-usage-accountant==0.0.10 simplejson==3.17.6 six==1.16.0 sniffio==1.2.0 -snuba-sdk==2.0.33 +snuba-sdk==2.0.34 sortedcontainers==2.4.0 soupsieve==2.3.2.post1 sqlparse==0.4.4 diff --git a/requirements-frozen.txt b/requirements-frozen.txt index bfb645df01a6da..dc97340ac8709e 100644 --- a/requirements-frozen.txt +++ b/requirements-frozen.txt @@ -129,7 +129,7 @@ sentry-usage-accountant==0.0.10 simplejson==3.17.6 six==1.16.0 sniffio==1.3.0 -snuba-sdk==2.0.33 +snuba-sdk==2.0.34 soupsieve==2.3.2.post1 sqlparse==0.4.4 statsd==3.3.0 From 51d0f0517f258ec5152df20c322ccac2419eef02 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Mon, 13 May 2024 14:32:41 -0400 Subject: [PATCH 08/31] re(crons): Consistent import of `.base` in endpoints (#70787) --- .../endpoints/monitor_ingest_checkin_attachment.py | 7 ++----- .../monitors/endpoints/organization_monitor_details.py | 5 +++-- .../endpoints/organization_monitor_environment_details.py | 7 +++---- src/sentry/monitors/endpoints/project_monitor_details.py | 5 +++-- .../endpoints/project_monitor_environment_details.py | 7 +++---- .../endpoints/project_monitor_processing_errors_index.py | 3 ++- .../endpoints/project_processing_errors_details.py | 3 ++- 7 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/sentry/monitors/endpoints/monitor_ingest_checkin_attachment.py b/src/sentry/monitors/endpoints/monitor_ingest_checkin_attachment.py index 19fc7fc53e61c7..ceb89788a34490 100644 --- a/src/sentry/monitors/endpoints/monitor_ingest_checkin_attachment.py +++ b/src/sentry/monitors/endpoints/monitor_ingest_checkin_attachment.py @@ -24,14 +24,11 @@ from sentry.models.organization import Organization from sentry.models.project import Project from sentry.models.projectkey import ProjectKey -from sentry.monitors.endpoints.base import ( - ProjectMonitorPermission, - get_monitor_by_org_id_or_slug, - try_checkin_lookup, -) from sentry.monitors.models import Monitor, MonitorCheckIn from sentry.utils.sdk import bind_organization_context +from .base import ProjectMonitorPermission, get_monitor_by_org_id_or_slug, try_checkin_lookup + MAX_ATTACHMENT_SIZE = 1024 * 100 # 100kb diff --git a/src/sentry/monitors/endpoints/organization_monitor_details.py b/src/sentry/monitors/endpoints/organization_monitor_details.py index f26db8299041eb..05f8bf95376fca 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_details.py +++ b/src/sentry/monitors/endpoints/organization_monitor_details.py @@ -15,12 +15,13 @@ RESPONSE_UNAUTHORIZED, ) from sentry.apidocs.parameters import GlobalParams, MonitorParams -from sentry.monitors.endpoints.base import MonitorEndpoint -from sentry.monitors.endpoints.base_monitor_details import MonitorDetailsMixin from sentry.monitors.serializers import MonitorSerializer from sentry.monitors.validators import MonitorValidator from sentry.utils.auth import AuthenticatedHttpRequest +from .base import MonitorEndpoint +from .base_monitor_details import MonitorDetailsMixin + @region_silo_endpoint @extend_schema(tags=["Crons"]) diff --git a/src/sentry/monitors/endpoints/organization_monitor_environment_details.py b/src/sentry/monitors/endpoints/organization_monitor_environment_details.py index 9c87b9294236d2..06037bde55ba16 100644 --- a/src/sentry/monitors/endpoints/organization_monitor_environment_details.py +++ b/src/sentry/monitors/endpoints/organization_monitor_environment_details.py @@ -15,12 +15,11 @@ RESPONSE_UNAUTHORIZED, ) from sentry.apidocs.parameters import GlobalParams, MonitorParams -from sentry.monitors.endpoints.base import MonitorEndpoint -from sentry.monitors.endpoints.base_monitor_environment_details import ( - MonitorEnvironmentDetailsMixin, -) from sentry.monitors.serializers import MonitorSerializer +from .base import MonitorEndpoint +from .base_monitor_environment_details import MonitorEnvironmentDetailsMixin + @region_silo_endpoint @extend_schema(tags=["Crons"]) diff --git a/src/sentry/monitors/endpoints/project_monitor_details.py b/src/sentry/monitors/endpoints/project_monitor_details.py index d4312e827b1059..639efbfae338bc 100644 --- a/src/sentry/monitors/endpoints/project_monitor_details.py +++ b/src/sentry/monitors/endpoints/project_monitor_details.py @@ -15,12 +15,13 @@ RESPONSE_UNAUTHORIZED, ) from sentry.apidocs.parameters import GlobalParams, MonitorParams -from sentry.monitors.endpoints.base import ProjectMonitorEndpoint -from sentry.monitors.endpoints.base_monitor_details import MonitorDetailsMixin from sentry.monitors.serializers import MonitorSerializer from sentry.monitors.validators import MonitorValidator from sentry.utils.auth import AuthenticatedHttpRequest +from .base import ProjectMonitorEndpoint +from .base_monitor_details import MonitorDetailsMixin + @region_silo_endpoint @extend_schema(tags=["Crons"]) diff --git a/src/sentry/monitors/endpoints/project_monitor_environment_details.py b/src/sentry/monitors/endpoints/project_monitor_environment_details.py index 0f050eed1d9f82..1ff7e28cb83de0 100644 --- a/src/sentry/monitors/endpoints/project_monitor_environment_details.py +++ b/src/sentry/monitors/endpoints/project_monitor_environment_details.py @@ -15,12 +15,11 @@ RESPONSE_UNAUTHORIZED, ) from sentry.apidocs.parameters import GlobalParams, MonitorParams -from sentry.monitors.endpoints.base import ProjectMonitorEnvironmentEndpoint -from sentry.monitors.endpoints.base_monitor_environment_details import ( - MonitorEnvironmentDetailsMixin, -) from sentry.monitors.serializers import MonitorSerializer +from .base import ProjectMonitorEnvironmentEndpoint +from .base_monitor_environment_details import MonitorEnvironmentDetailsMixin + @region_silo_endpoint @extend_schema(tags=["Crons"]) diff --git a/src/sentry/monitors/endpoints/project_monitor_processing_errors_index.py b/src/sentry/monitors/endpoints/project_monitor_processing_errors_index.py index eaadb96874455a..c7b516a0b37709 100644 --- a/src/sentry/monitors/endpoints/project_monitor_processing_errors_index.py +++ b/src/sentry/monitors/endpoints/project_monitor_processing_errors_index.py @@ -9,13 +9,14 @@ from sentry.apidocs.constants import RESPONSE_FORBIDDEN, RESPONSE_NOT_FOUND, RESPONSE_UNAUTHORIZED from sentry.apidocs.parameters import GlobalParams, MonitorParams from sentry.apidocs.utils import inline_sentry_response_serializer -from sentry.monitors.endpoints.base import ProjectMonitorEndpoint from sentry.monitors.processing_errors import ( CheckinProcessErrorsManager, CheckinProcessingErrorData, ) from sentry.utils.auth import AuthenticatedHttpRequest +from .base import ProjectMonitorEndpoint + @region_silo_endpoint @extend_schema(tags=["Crons"]) diff --git a/src/sentry/monitors/endpoints/project_processing_errors_details.py b/src/sentry/monitors/endpoints/project_processing_errors_details.py index 3a85add3a80eca..67169046a9b4a4 100644 --- a/src/sentry/monitors/endpoints/project_processing_errors_details.py +++ b/src/sentry/monitors/endpoints/project_processing_errors_details.py @@ -20,9 +20,10 @@ ) from sentry.apidocs.parameters import GlobalParams, MonitorParams from sentry.models.project import Project -from sentry.monitors.endpoints.base import ProjectMonitorPermission from sentry.monitors.processing_errors import CheckinProcessErrorsManager, InvalidProjectError +from .base import ProjectMonitorPermission + @region_silo_endpoint @extend_schema(tags=["Crons"]) From 716dcf2e5ee429b177dcdb576794fb6637af33e4 Mon Sep 17 00:00:00 2001 From: Raj Joshi Date: Mon, 13 May 2024 11:38:17 -0700 Subject: [PATCH 09/31] feat(chartcuterie): Refactor Function Regression Chart to separate data (#70250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I'm adding Chartcuterie support for Function Regression chart. The first step is to refactor it so we can generate the chart props in the backend. I updated the `getBreakpointChartOptionsFromData` function to remove backwards compatibility for `functionBreakpointChart` since I am updating it to move all the transformation to a separate function. I tested using `yarn dev-ui`: image image Preview of the Slack Image 👀 ![example3](https://github.com/getsentry/sentry/assets/33237075/de8da223-13b4-427e-a2e4-4220ef256a05) --- static/app/chartcuterie/performance.tsx | 55 +++++++- static/app/chartcuterie/types.tsx | 1 + .../breakpointChart.tsx | 2 + .../breakpointChartOptions.tsx | 44 ++++-- .../functionBreakpointChart.tsx | 19 +-- .../eventStatisticalDetector/lineChart.tsx | 24 ++-- .../profiling/hooks/useProfileEventsStats.tsx | 125 +----------------- .../hooks/useProfileTopEventsStats.tsx | 2 +- static/app/utils/profiling/hooks/utils.tsx | 122 +++++++++++++++++ 9 files changed, 229 insertions(+), 165 deletions(-) diff --git a/static/app/chartcuterie/performance.tsx b/static/app/chartcuterie/performance.tsx index 452d440eecddcf..b2c9240c528e31 100644 --- a/static/app/chartcuterie/performance.tsx +++ b/static/app/chartcuterie/performance.tsx @@ -3,7 +3,10 @@ import {transformToLineSeries} from 'sentry/components/charts/lineChart'; import getBreakpointChartOptionsFromData, { type EventBreakpointChartData, } from 'sentry/components/events/eventStatisticalDetector/breakpointChartOptions'; +import type {EventsStatsSeries} from 'sentry/types'; +import {transformStatsResponse} from 'sentry/utils/profiling/hooks/utils'; import {lightTheme as theme} from 'sentry/utils/theme'; +import type {NormalizedTrendsTransaction} from 'sentry/views/performance/trends/types'; import {slackChartDefaults, slackChartSize} from './slack'; import type {RenderDescriptor} from './types'; @@ -11,6 +14,10 @@ import {ChartType} from './types'; export const performanceCharts: RenderDescriptor[] = []; +export type FunctionRegressionPercentileData = { + data: EventsStatsSeries<'p95()'>; +}; + function modifyOptionsForSlack(options: Omit) { options.legend = options.legend || {}; options.legend.icon = 'none'; @@ -23,11 +30,57 @@ function modifyOptionsForSlack(options: Omit) { visualMap: options.options?.visualMap, }; } +type FunctionRegressionChartData = { + evidenceData: NormalizedTrendsTransaction; + rawResponse: any; +}; performanceCharts.push({ key: ChartType.SLACK_PERFORMANCE_ENDPOINT_REGRESSION, getOption: (data: EventBreakpointChartData) => { - const {chartOptions, series} = getBreakpointChartOptionsFromData(data, theme); + const {chartOptions, series} = getBreakpointChartOptionsFromData( + data, + ChartType.SLACK_PERFORMANCE_ENDPOINT_REGRESSION, + theme + ); + const transformedSeries = transformToLineSeries({series}); + const modifiedOptions = modifyOptionsForSlack(chartOptions); + + return { + ...modifiedOptions, + + backgroundColor: theme.background, + series: transformedSeries, + grid: slackChartDefaults.grid, + visualMap: modifiedOptions.options?.visualMap, + }; + }, + ...slackChartSize, +}); + +performanceCharts.push({ + key: ChartType.SLACK_PERFORMANCE_FUNCTION_REGRESSION, + getOption: (data: FunctionRegressionChartData) => { + const transformed = transformStatsResponse( + 'profileFunctions', + ['p95()'], + data.rawResponse + ); + + const percentileData = { + data: transformed, + }; + + const param = { + percentileData: percentileData as FunctionRegressionPercentileData, + evidenceData: data.evidenceData, + }; + + const {chartOptions, series} = getBreakpointChartOptionsFromData( + param, + ChartType.SLACK_PERFORMANCE_FUNCTION_REGRESSION, + theme + ); const transformedSeries = transformToLineSeries({series}); const modifiedOptions = modifyOptionsForSlack(chartOptions); diff --git a/static/app/chartcuterie/types.tsx b/static/app/chartcuterie/types.tsx index 7bd1c2ee7c8024..526a15747f79b8 100644 --- a/static/app/chartcuterie/types.tsx +++ b/static/app/chartcuterie/types.tsx @@ -17,6 +17,7 @@ export enum ChartType { SLACK_METRIC_ALERT_EVENTS = 'slack:metricAlert.events', SLACK_METRIC_ALERT_SESSIONS = 'slack:metricAlert.sessions', SLACK_PERFORMANCE_ENDPOINT_REGRESSION = 'slack:performance.endpointRegression', + SLACK_PERFORMANCE_FUNCTION_REGRESSION = 'slack:performance.functionRegression', } /** diff --git a/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx b/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx index b4e9a83bce88c6..2c9ce2fc6633e5 100644 --- a/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx +++ b/static/app/components/events/eventStatisticalDetector/breakpointChart.tsx @@ -1,3 +1,4 @@ +import {ChartType} from 'sentry/chartcuterie/types'; import TransitionChart from 'sentry/components/charts/transitionChart'; import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask'; import type {Event, EventsStatsData} from 'sentry/types'; @@ -80,6 +81,7 @@ function EventBreakpointChart({event}: EventBreakpointChartProps) { percentileData={data?.['p95(transaction.duration)']?.data ?? []} evidenceData={normalizedOccurrenceEvent} datetime={datetime} + chartType={ChartType.SLACK_PERFORMANCE_ENDPOINT_REGRESSION} /> diff --git a/static/app/components/events/eventStatisticalDetector/breakpointChartOptions.tsx b/static/app/components/events/eventStatisticalDetector/breakpointChartOptions.tsx index ea5559b4c8831f..13df1d1dc777e1 100644 --- a/static/app/components/events/eventStatisticalDetector/breakpointChartOptions.tsx +++ b/static/app/components/events/eventStatisticalDetector/breakpointChartOptions.tsx @@ -1,8 +1,9 @@ import type {Theme} from '@emotion/react'; +import type {FunctionRegressionPercentileData} from 'sentry/chartcuterie/performance'; +import {ChartType} from 'sentry/chartcuterie/types'; import VisualMap from 'sentry/components/charts/components/visualMap'; import type {LineChart as EChartsLineChart} from 'sentry/components/charts/lineChart'; -import type {Series} from 'sentry/types/echarts'; import type {EventsStatsData} from 'sentry/types/organization'; import { axisLabelFormatter, @@ -20,22 +21,41 @@ import {getIntervalLine} from 'sentry/views/performance/utils/getIntervalLine'; export type EventBreakpointChartData = { evidenceData: NormalizedTrendsTransaction; - percentileData?: EventsStatsData; - percentileSeries?: Series[]; + percentileData: EventsStatsData | FunctionRegressionPercentileData; }; function getBreakpointChartOptionsFromData( - {percentileData, evidenceData, percentileSeries}: EventBreakpointChartData, + {percentileData, evidenceData}: EventBreakpointChartData, + chartType: ChartType, theme: Theme ) { - const transformedSeries = percentileData - ? transformEventStats( - percentileData, - generateTrendFunctionAsString(TrendFunctionField.P95, 'transaction.duration') - ) - : percentileSeries - ? percentileSeries - : []; + const trendFunctionName: Partial<{[key in ChartType]: string}> = { + [ChartType.SLACK_PERFORMANCE_ENDPOINT_REGRESSION]: 'transaction.duration', + [ChartType.SLACK_PERFORMANCE_FUNCTION_REGRESSION]: 'function.duration', + }; + + const defaultTransform = stats => stats; + + const transformFunctionStats = (stats: any) => { + const rawData = stats?.data?.data?.find(({axis}) => axis === 'p95()'); + const timestamps = stats?.data?.timestamps; + if (!timestamps) { + return []; + } + return timestamps.map((timestamp, i) => [timestamp, [{count: rawData.values[i]}]]); + }; + + // Mapping from BreakpointType to transformation functions + const transformFunction: Partial<{[key in ChartType]: (arg: any) => EventsStatsData}> = + { + [ChartType.SLACK_PERFORMANCE_ENDPOINT_REGRESSION]: defaultTransform, + [ChartType.SLACK_PERFORMANCE_FUNCTION_REGRESSION]: transformFunctionStats, + }; + + const transformedSeries = transformEventStats( + transformFunction[chartType]!(percentileData), + generateTrendFunctionAsString(TrendFunctionField.P95, trendFunctionName[chartType]!) + ); const intervalSeries = getIntervalLine( theme, diff --git a/static/app/components/events/eventStatisticalDetector/functionBreakpointChart.tsx b/static/app/components/events/eventStatisticalDetector/functionBreakpointChart.tsx index 7f2fc8f7d67901..4103edf7961754 100644 --- a/static/app/components/events/eventStatisticalDetector/functionBreakpointChart.tsx +++ b/static/app/components/events/eventStatisticalDetector/functionBreakpointChart.tsx @@ -1,6 +1,7 @@ -import {useEffect, useMemo} from 'react'; +import {useEffect} from 'react'; import * as Sentry from '@sentry/react'; +import {ChartType} from 'sentry/chartcuterie/types'; import Chart from 'sentry/components/events/eventStatisticalDetector/lineChart'; import {DataSection} from 'sentry/components/events/styles'; import type {Event} from 'sentry/types/event'; @@ -8,7 +9,6 @@ import {defined} from 'sentry/utils'; import {useProfileEventsStats} from 'sentry/utils/profiling/hooks/useProfileEventsStats'; import {useRelativeDateTime} from 'sentry/utils/profiling/hooks/useRelativeDateTime'; import type {NormalizedTrendsTransaction} from 'sentry/views/performance/trends/types'; -import transformEventStats from 'sentry/views/performance/trends/utils/transformEventStats'; import {RELATIVE_DAYS_WINDOW} from './consts'; @@ -75,18 +75,6 @@ function EventFunctionBreakpointChartInner({ yAxes: SERIES, }); - const p95Series = useMemo(() => { - const rawData = functionStats?.data?.data?.find(({axis}) => axis === 'p95()'); - const timestamps = functionStats?.data?.timestamps; - if (!timestamps) { - return []; - } - return transformEventStats( - timestamps.map((timestamp, i) => [timestamp, [{count: rawData.values[i]}]]), - 'p95(function.duration)' - ); - }, [functionStats]); - const normalizedOccurrenceEvent = { aggregate_range_1: evidenceData.aggregateRange1 / 1e6, aggregate_range_2: evidenceData.aggregateRange2 / 1e6, @@ -96,9 +84,10 @@ function EventFunctionBreakpointChartInner({ return ( ); diff --git a/static/app/components/events/eventStatisticalDetector/lineChart.tsx b/static/app/components/events/eventStatisticalDetector/lineChart.tsx index b274c2e32ee24e..9988f34fc99b27 100644 --- a/static/app/components/events/eventStatisticalDetector/lineChart.tsx +++ b/static/app/components/events/eventStatisticalDetector/lineChart.tsx @@ -1,37 +1,35 @@ import {useMemo} from 'react'; import {useTheme} from '@emotion/react'; +import type {FunctionRegressionPercentileData} from 'sentry/chartcuterie/performance'; +import type {ChartType} from 'sentry/chartcuterie/types'; import ChartZoom from 'sentry/components/charts/chartZoom'; import {LineChart as EChartsLineChart} from 'sentry/components/charts/lineChart'; import getBreakpointChartOptionsFromData from 'sentry/components/events/eventStatisticalDetector/breakpointChartOptions'; -import type {EventsStatsData, PageFilters} from 'sentry/types'; -import type {Series} from 'sentry/types/echarts'; +import type {PageFilters} from 'sentry/types'; +import type {EventsStatsData} from 'sentry/types/organization'; import useRouter from 'sentry/utils/useRouter'; import type {NormalizedTrendsTransaction} from 'sentry/views/performance/trends/types'; interface ChartProps { + chartType: ChartType; datetime: PageFilters['datetime']; evidenceData: NormalizedTrendsTransaction; - // TODO @athena: Refactor functionBreakpointChart to use percentileData - percentileData?: EventsStatsData; - percentileSeries?: Series[]; + percentileData: EventsStatsData | FunctionRegressionPercentileData; + trendFunctionName?: string; } -function LineChart({ - datetime, - percentileData, - percentileSeries, - evidenceData, -}: ChartProps) { +function LineChart({datetime, percentileData, evidenceData, chartType}: ChartProps) { const theme = useTheme(); const router = useRouter(); const {series, chartOptions} = useMemo(() => { return getBreakpointChartOptionsFromData( - {percentileData, percentileSeries, evidenceData}, + {percentileData, evidenceData}, + chartType, theme ); - }, [percentileData, percentileSeries, evidenceData, theme]); + }, [percentileData, evidenceData, chartType, theme]); return ( diff --git a/static/app/utils/profiling/hooks/useProfileEventsStats.tsx b/static/app/utils/profiling/hooks/useProfileEventsStats.tsx index 9e4ebf9221a898..6841a60ea8c317 100644 --- a/static/app/utils/profiling/hooks/useProfileEventsStats.tsx +++ b/static/app/utils/profiling/hooks/useProfileEventsStats.tsx @@ -1,10 +1,8 @@ import {useMemo} from 'react'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; -import type {EventsStatsSeries, PageFilters} from 'sentry/types'; -import {defined} from 'sentry/utils'; -import {getAggregateAlias} from 'sentry/utils/discover/fields'; -import {makeFormatTo} from 'sentry/utils/profiling/units/units'; +import type {PageFilters} from 'sentry/types'; +import {transformStatsResponse} from 'sentry/utils/profiling/hooks/utils'; import {useApiQuery} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; @@ -73,122 +71,3 @@ export function useProfileEventsStats({ ...rest, }; } - -export function transformStatsResponse( - dataset: 'discover' | 'profiles' | 'profileFunctions', - yAxes: readonly F[], - rawData: any -): EventsStatsSeries { - // the events stats endpoint has a legacy response format so here we transform it - // into the proposed update for forward compatibility and ease of use - - if (yAxes.length === 0) { - return { - data: [], - meta: { - dataset, - end: 0, - start: 0, - }, - timestamps: [], - }; - } - - if (yAxes.length === 1) { - const {series, meta, timestamps} = transformSingleSeries(dataset, yAxes[0], rawData); - return { - data: [series], - meta, - timestamps, - }; - } - - const data: EventsStatsSeries['data'] = []; - let meta: EventsStatsSeries['meta'] = { - dataset, - end: -1, - start: -1, - }; - let timestamps: EventsStatsSeries['timestamps'] = []; - - let firstAxis = true; - - for (const yAxis of yAxes) { - const dataForYAxis = rawData[yAxis]; - if (!defined(dataForYAxis)) { - continue; - } - const transformed = transformSingleSeries(dataset, yAxis, dataForYAxis); - - if (firstAxis) { - meta = transformed.meta; - timestamps = transformed.timestamps; - } else if ( - meta.start !== transformed.meta.start || - meta.end !== transformed.meta.end - ) { - throw new Error('Mismatching start/end times'); - } else if ( - timestamps.length !== transformed.timestamps.length || - timestamps.some((ts, i) => ts !== transformed.timestamps[i]) - ) { - throw new Error('Mismatching timestamps'); - } - - data.push(transformed.series); - - firstAxis = false; - } - - return { - data, - meta, - timestamps, - }; -} - -export function transformSingleSeries( - dataset: 'discover' | 'profiles' | 'profileFunctions', - yAxis: F, - rawSeries: any, - label?: string -) { - const type = - rawSeries.meta.fields[yAxis] ?? rawSeries.meta.fields[getAggregateAlias(yAxis)]; - const formatter = - type === 'duration' - ? makeFormatTo( - rawSeries.meta.units[yAxis] ?? - rawSeries.meta.units[getAggregateAlias(yAxis)] ?? - 'nanoseconds', - 'milliseconds' - ) - : type === 'string' - ? value => value || '' - : value => value; - - const series: EventsStatsSeries['data'][number] = { - axis: yAxis, - values: [], - label, - }; - const meta: EventsStatsSeries['meta'] = { - dataset, - end: rawSeries.end, - start: rawSeries.start, - }; - const timestamps: EventsStatsSeries['timestamps'] = []; - - for (let i = 0; i < rawSeries.data.length; i++) { - const [timestamp, value] = rawSeries.data[i]; - // the api has this awkward structure for legacy reason - series.values.push(formatter(value[0].count as number)); - timestamps.push(timestamp); - } - - return { - series, - meta, - timestamps, - }; -} diff --git a/static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx b/static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx index ccfce76a67c8b1..96f8b512c07340 100644 --- a/static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx +++ b/static/app/utils/profiling/hooks/useProfileTopEventsStats.tsx @@ -3,7 +3,7 @@ import {useMemo} from 'react'; import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; import type {EventsStatsSeries, PageFilters} from 'sentry/types'; import {defined} from 'sentry/utils'; -import {transformSingleSeries} from 'sentry/utils/profiling/hooks/useProfileEventsStats'; +import {transformSingleSeries} from 'sentry/utils/profiling/hooks/utils'; import {useApiQuery} from 'sentry/utils/queryClient'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; diff --git a/static/app/utils/profiling/hooks/utils.tsx b/static/app/utils/profiling/hooks/utils.tsx index 65ab1eec465bb7..752ec270119c6a 100644 --- a/static/app/utils/profiling/hooks/utils.tsx +++ b/static/app/utils/profiling/hooks/utils.tsx @@ -1,5 +1,8 @@ import {t} from 'sentry/locale'; +import type {EventsStatsSeries} from 'sentry/types'; import {defined} from 'sentry/utils'; +import {getAggregateAlias} from 'sentry/utils/discover/fields'; +import {makeFormatTo} from 'sentry/utils/profiling/units/units'; import type {Sort} from './types'; @@ -36,3 +39,122 @@ export function formatError(error: any): string | null { return t('An unknown error occurred.'); } + +export function transformStatsResponse( + dataset: 'discover' | 'profiles' | 'profileFunctions', + yAxes: readonly F[], + rawData: any +): EventsStatsSeries { + // the events stats endpoint has a legacy response format so here we transform it + // into the proposed update for forward compatibility and ease of use + + if (yAxes.length === 0) { + return { + data: [], + meta: { + dataset, + end: 0, + start: 0, + }, + timestamps: [], + }; + } + + if (yAxes.length === 1) { + const {series, meta, timestamps} = transformSingleSeries(dataset, yAxes[0], rawData); + return { + data: [series], + meta, + timestamps, + }; + } + + const data: EventsStatsSeries['data'] = []; + let meta: EventsStatsSeries['meta'] = { + dataset, + end: -1, + start: -1, + }; + let timestamps: EventsStatsSeries['timestamps'] = []; + + let firstAxis = true; + + for (const yAxis of yAxes) { + const dataForYAxis = rawData[yAxis]; + if (!defined(dataForYAxis)) { + continue; + } + const transformed = transformSingleSeries(dataset, yAxis, dataForYAxis); + + if (firstAxis) { + meta = transformed.meta; + timestamps = transformed.timestamps; + } else if ( + meta.start !== transformed.meta.start || + meta.end !== transformed.meta.end + ) { + throw new Error('Mismatching start/end times'); + } else if ( + timestamps.length !== transformed.timestamps.length || + timestamps.some((ts, i) => ts !== transformed.timestamps[i]) + ) { + throw new Error('Mismatching timestamps'); + } + + data.push(transformed.series); + + firstAxis = false; + } + + return { + data, + meta, + timestamps, + }; +} + +export function transformSingleSeries( + dataset: 'discover' | 'profiles' | 'profileFunctions', + yAxis: F, + rawSeries: any, + label?: string +) { + const type = + rawSeries.meta.fields[yAxis] ?? rawSeries.meta.fields[getAggregateAlias(yAxis)]; + const formatter = + type === 'duration' + ? makeFormatTo( + rawSeries.meta.units[yAxis] ?? + rawSeries.meta.units[getAggregateAlias(yAxis)] ?? + 'nanoseconds', + 'milliseconds' + ) + : type === 'string' + ? value => value || '' + : value => value; + + const series: EventsStatsSeries['data'][number] = { + axis: yAxis, + values: [], + label, + }; + const meta: EventsStatsSeries['meta'] = { + dataset, + end: rawSeries.end, + start: rawSeries.start, + }; + const timestamps: EventsStatsSeries['timestamps'] = []; + + for (let i = 0; i < rawSeries.data.length; i++) { + const [timestamp, value] = rawSeries.data[i]; + // the api has this awkward structure for legacy reason + series.values.push(formatter(value[0].count as number)); + timestamps.push(timestamp); + } + + return { + series, + meta, + timestamps, + }; +} From 93405b6da3f360c6e08b199862927e24e3c4ca5d Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 13 May 2024 14:41:54 -0400 Subject: [PATCH 10/31] fix(feedback): fix types for checking if a feedback has a screenshot (#70779) follow up to https://github.com/getsentry/sentry/pull/70778 - just changing the type name --- static/app/components/feedback/list/feedbackListItem.tsx | 2 +- static/app/components/feedback/useFeedbackListQueryKey.tsx | 2 +- static/app/types/group.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/app/components/feedback/list/feedbackListItem.tsx b/static/app/components/feedback/list/feedbackListItem.tsx index 73099836fe69c6..8ca33e99d08548 100644 --- a/static/app/components/feedback/list/feedbackListItem.tsx +++ b/static/app/components/feedback/list/feedbackListItem.tsx @@ -56,7 +56,7 @@ const FeedbackListItem = forwardRef( const isCrashReport = feedbackItem.metadata.source === 'crash_report_embed_form'; const isUserReportWithError = feedbackItem.metadata.source === 'user_report_envelope'; - const hasAttachments = feedbackItem.hasAttachments; + const hasAttachments = feedbackItem.latestEventHasAttachments; const hasComments = feedbackItem.numComments > 0; const theme = isOpen || config.theme === 'dark' ? darkTheme : lightTheme; diff --git a/static/app/components/feedback/useFeedbackListQueryKey.tsx b/static/app/components/feedback/useFeedbackListQueryKey.tsx index 74aa34e1fa68b0..bd6b239f018c5c 100644 --- a/static/app/components/feedback/useFeedbackListQueryKey.tsx +++ b/static/app/components/feedback/useFeedbackListQueryKey.tsx @@ -94,7 +94,7 @@ export default function useFeedbackListQueryKey({ 'pluginIssues', // Gives us plugin issues available 'integrationIssues', // Gives us integration issues available 'sentryAppIssues', // Gives us Sentry app issues available - 'hasAttachments', // Gives us whether the feedback has screenshots + 'latestEventHasAttachments', // Gives us whether the feedback has screenshots ], shortIdLookup: 0, query: `issue.category:feedback status:${mailbox} ${fixedQueryView.query}`, diff --git a/static/app/types/group.tsx b/static/app/types/group.tsx index b155ab26f8b53c..8f8e896a354a17 100644 --- a/static/app/types/group.tsx +++ b/static/app/types/group.tsx @@ -804,10 +804,10 @@ export interface BaseGroup { title: string; type: EventOrGroupType; userReportCount: number; - hasAttachments?: boolean; inbox?: InboxDetails | null | false; integrationIssues?: ExternalIssue[]; latestEvent?: Event; + latestEventHasAttachments?: boolean; owners?: SuggestedOwner[] | null; sentryAppIssues?: PlatformExternalIssue[]; substatus?: GroupSubstatus | null; From 52648937e5efc299de8d480ae6864ff888bc10fa Mon Sep 17 00:00:00 2001 From: Michael Sun <55160142+MichaelSun48@users.noreply.github.com> Date: Mon, 13 May 2024 11:47:12 -0700 Subject: [PATCH 11/31] fix(assignee-badge): Fix storybook issues (#70775) Fixes JAVASCRIPT-2T1X and an issue where the title for the AssigneeBadge's storybook showed up as `` instead of `` --- .../app/components/assigneeBadge.stories.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/static/app/components/assigneeBadge.stories.tsx b/static/app/components/assigneeBadge.stories.tsx index 7b78f2627cad5d..e2e990ab7b117e 100644 --- a/static/app/components/assigneeBadge.stories.tsx +++ b/static/app/components/assigneeBadge.stories.tsx @@ -1,12 +1,13 @@ import {Fragment, useState} from 'react'; +import {uuid4} from '@sentry/utils'; import {AssigneeBadge} from 'sentry/components/assigneeBadge'; import storyBook from 'sentry/stories/storyBook'; -import type {Actor} from 'sentry/types'; +import type {Actor, Team} from 'sentry/types'; import {useUser} from 'sentry/utils/useUser'; import {useUserTeams} from 'sentry/utils/useUserTeams'; -export default storyBook(AssigneeBadge, story => { +export default storyBook('AssigneeBadge', story => { story('User Assignee', () => { const user = useUser(); const [chevron1Toggle, setChevron1Toggle] = useState<'up' | 'down'>('down'); @@ -39,10 +40,29 @@ export default storyBook(AssigneeBadge, story => { const [chevron1Toggle, setChevron1Toggle] = useState<'up' | 'down'>('down'); const [chevron2Toggle, setChevron2Toggle] = useState<'up' | 'down'>('down'); + const team: Team = teams.length + ? teams[0] + : { + id: '1', + slug: 'team-slug', + name: 'Team Name', + access: ['team:read'], + teamRole: null, + isMember: true, + memberCount: 0, + avatar: {avatarType: 'letter_avatar', avatarUuid: uuid4()}, + flags: { + 'idp:provisioned': false, + }, + externalTeams: [], + hasAccess: false, + isPending: false, + }; + const teamActor: Actor = { type: 'team', - id: teams[0].id, - name: teams[0].name, + id: team.id, + name: team.name, }; return ( From d0993758949430abedcf99301311fa3a0ba27c97 Mon Sep 17 00:00:00 2001 From: Matej Minar Date: Mon, 13 May 2024 20:55:41 +0200 Subject: [PATCH 12/31] chore(metrics): Remove metrics-stats flag on backend (#70764) Closes https://github.com/getsentry/sentry/issues/70724 Requires https://github.com/getsentry/getsentry/pull/13934 --- src/sentry/api/endpoints/organization_stats_v2.py | 2 +- src/sentry/conf/server.py | 2 -- src/sentry/features/temporary.py | 1 - tests/snuba/api/endpoints/test_organization_stats_v2.py | 6 +++--- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/sentry/api/endpoints/organization_stats_v2.py b/src/sentry/api/endpoints/organization_stats_v2.py index 31ae658a810996..e43595649c3fb7 100644 --- a/src/sentry/api/endpoints/organization_stats_v2.py +++ b/src/sentry/api/endpoints/organization_stats_v2.py @@ -167,7 +167,7 @@ def get(self, request: Request, organization) -> Response: """ with self.handle_query_errors(): - if features.has("organizations:metrics-stats", organization): + if features.has("organizations:custom-metrics", organization): if ( request.GET.get("category") == "metrics" or request.GET.get("category") == "metricSecond" diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 930fd60b18657a..215d51c246806b 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -1521,8 +1521,6 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: "organizations:ddm-sidebar-item-hidden": False, # Enables import of metric dashboards "organizations:ddm-dashboard-import": False, - # Enables category "metrics" in stats_v2 endpoint - "organizations:metrics-stats": False, # Enable the default alert at project creation to be the high priority alert "organizations:default-high-priority-alerts": False, # Enables automatically deriving of code mappings diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index 395cd93c605072..605a4c4912c667 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -61,7 +61,6 @@ def register_temporary_features(manager: FeatureManager): manager.add("organizations:ddm-dashboard-import", OrganizationFeature, FeatureHandlerStrategy.REMOTE) manager.add("organizations:custom-metrics-experimental", OrganizationFeature, FeatureHandlerStrategy.REMOTE) manager.add("organizations:ddm-sidebar-item-hidden", OrganizationFeature, FeatureHandlerStrategy.REMOTE) - manager.add("organizations:metrics-stats", OrganizationFeature, FeatureHandlerStrategy.REMOTE) manager.add("organizations:default-high-priority-alerts", OrganizationFeature, FeatureHandlerStrategy.INTERNAL) manager.add("organizations:derive-code-mappings", OrganizationFeature, FeatureHandlerStrategy.INTERNAL) manager.add("organizations:device-class-synthesis", OrganizationFeature, FeatureHandlerStrategy.INTERNAL) diff --git a/tests/snuba/api/endpoints/test_organization_stats_v2.py b/tests/snuba/api/endpoints/test_organization_stats_v2.py index dca7f15a90c9a6..37e3ed82b6e03a 100644 --- a/tests/snuba/api/endpoints/test_organization_stats_v2.py +++ b/tests/snuba/api/endpoints/test_organization_stats_v2.py @@ -951,7 +951,7 @@ def setUp(self): ) @freeze_time("2021-03-14T12:27:28.303Z") - @with_feature("organizations:metrics-stats") + @with_feature("organizations:custom-metrics") def test_metrics_category(self): response = self.do_request( { @@ -974,7 +974,7 @@ def test_metrics_category(self): } @freeze_time("2021-03-14T12:27:28.303Z") - @with_feature("organizations:metrics-stats") + @with_feature("organizations:custom-metrics") def test_metrics_group_by_project(self): response = self.do_request( { @@ -1007,7 +1007,7 @@ def test_metrics_group_by_project(self): } @freeze_time("2021-03-14T12:27:28.303Z") - @with_feature("organizations:metrics-stats") + @with_feature("organizations:custom-metrics") def test_metrics_multiple_group_by(self): response = self.do_request( { From f5b1a7738d8c13de2636438824caa2a238b7970c Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 13 May 2024 15:00:36 -0400 Subject: [PATCH 13/31] ref(tsc): convert adminSettings.tsx to FC (#70792) ref https://github.com/getsentry/frontend-tsc/issues/2 converts this file into FC and use `useApiQuery` instead of `DeprecatedAsync` --- static/app/views/admin/adminSettings.tsx | 299 ++++++++++++----------- 1 file changed, 152 insertions(+), 147 deletions(-) diff --git a/static/app/views/admin/adminSettings.tsx b/static/app/views/admin/adminSettings.tsx index 384490e1840be7..19f3d932adc2a6 100644 --- a/static/app/views/admin/adminSettings.tsx +++ b/static/app/views/admin/adminSettings.tsx @@ -1,9 +1,11 @@ import Feature from 'sentry/components/acl/feature'; import Form from 'sentry/components/forms/form'; +import LoadingError from 'sentry/components/loadingError'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; import Panel from 'sentry/components/panels/panel'; import PanelHeader from 'sentry/components/panels/panelHeader'; import {t} from 'sentry/locale'; -import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView'; +import {useApiQuery} from 'sentry/utils/queryClient'; import {getOption, getOptionField} from './options'; @@ -67,165 +69,168 @@ type FieldDef = { value: string | undefined; }; -type State = DeprecatedAsyncView['state'] & { - data: Record; -}; +export default function AdminSettings() { + const {data, isLoading, isError} = useApiQuery>( + ['/internal/options/'], + { + staleTime: 0, + } + ); -export default class AdminSettings extends DeprecatedAsyncView<{}, State> { - get endpoint() { - return '/internal/options/'; + if (isError) { + return ; } - getEndpoints(): ReturnType { - return [['data', this.endpoint]]; + if (isLoading) { + return ; } - renderBody() { - const {data} = this.state; - - const initialData = {}; - const fields = {}; - for (const key of optionsAvailable) { - // TODO(dcramer): we should not be mutating options - const option = data[key] ?? {field: {}, value: undefined}; + const initialData = {}; + const fields = {}; + for (const key of optionsAvailable) { + // TODO(dcramer): we should not be mutating options + const option = data[key] ?? {field: {}, value: undefined}; - if (option.value === undefined || option.value === '') { - const defn = getOption(key); - initialData[key] = defn.defaultValue ? defn.defaultValue() : ''; - } else { - initialData[key] = option.value; - } - fields[key] = getOptionField(key, option.field); + if (option.value === undefined || option.value === '') { + const defn = getOption(key); + initialData[key] = defn.defaultValue ? defn.defaultValue() : ''; + } else { + initialData[key] = option.value; } + fields[key] = getOptionField(key, option.field); + } + + return ( +
+

{t('Settings')}

- return ( -
-

{t('Settings')}

+
+ + {t('General')} + {fields['system.url-prefix']} + {fields['system.admin-email']} + {fields['system.support-email']} + {fields['system.security-email']} + {fields['system.rate-limit']} + - + + {t('Security & Abuse')} + {fields['auth.allow-registration']} + {fields['auth.ip-rate-limit']} + {fields['auth.user-rate-limit']} + {fields['api.rate-limit.org-create']} + + + + {t('Beacon')} + {fields['beacon.anonymous']} + + + - General - {fields['system.url-prefix']} - {fields['system.admin-email']} - {fields['system.support-email']} - {fields['system.security-email']} - {fields['system.rate-limit']} + {t('Performance Issues - All')} + {fields['performance.issues.all.problem-detection']} - - Security & Abuse - {fields['auth.allow-registration']} - {fields['auth.ip-rate-limit']} - {fields['auth.user-rate-limit']} - {fields['api.rate-limit.org-create']} + {t('Performance Issues - Detectors')} + {fields['performance.issues.n_plus_one_db.problem-creation']} + {fields['performance.issues.n_plus_one_db_ext.problem-creation']} + {fields['performance.issues.n_plus_one_db.count_threshold']} + {fields['performance.issues.n_plus_one_db.duration_threshold']} - - Beacon - {fields['beacon.anonymous']} + {t('Performance Issues - Consecutive DB Detector')} + {fields['performance.issues.consecutive_db.problem-creation']} + {fields['performance.issues.consecutive_db.la-rollout']} + {fields['performance.issues.consecutive_db.ea-rollout']} + {fields['performance.issues.consecutive_db.ga-rollout']} - - - - Performance Issues - All - {fields['performance.issues.all.problem-detection']} - - - Performance Issues - Detectors - {fields['performance.issues.n_plus_one_db.problem-creation']} - {fields['performance.issues.n_plus_one_db_ext.problem-creation']} - {fields['performance.issues.n_plus_one_db.count_threshold']} - {fields['performance.issues.n_plus_one_db.duration_threshold']} - - - Performance Issues - Consecutive DB Detector - {fields['performance.issues.consecutive_db.problem-creation']} - {fields['performance.issues.consecutive_db.la-rollout']} - {fields['performance.issues.consecutive_db.ea-rollout']} - {fields['performance.issues.consecutive_db.ga-rollout']} - - - Performance Issues - N+1 API Calls Detector - {fields['performance.issues.n_plus_one_api_calls.problem-creation']} - {fields['performance.issues.n_plus_one_api_calls.la-rollout']} - {fields['performance.issues.n_plus_one_api_calls.ea-rollout']} - {fields['performance.issues.n_plus_one_api_calls.ga-rollout']} - - - Performance Issues - Compressed Assets Detector - {fields['performance.issues.compressed_assets.problem-creation']} - {fields['performance.issues.compressed_assets.la-rollout']} - {fields['performance.issues.compressed_assets.ea-rollout']} - {fields['performance.issues.compressed_assets.ga-rollout']} - - - Performance Issues - File IO on Main Thread - {fields['performance.issues.file_io_main_thread.problem-creation']} - - - Performance Issues - Slow DB Span Detector - {fields['performance.issues.slow_db_query.problem-creation']} - {fields['performance.issues.slow_db_query.la-rollout']} - {fields['performance.issues.slow_db_query.ea-rollout']} - {fields['performance.issues.slow_db_query.ga-rollout']} - - - - Performance Issues - Large Render Blocking Asset Detector - - {fields['performance.issues.render_blocking_assets.problem-creation']} - {fields['performance.issues.render_blocking_assets.la-rollout']} - {fields['performance.issues.render_blocking_assets.ea-rollout']} - {fields['performance.issues.render_blocking_assets.ga-rollout']} - - - Performance Issues - MN+1 DB Detector - {fields['performance.issues.m_n_plus_one_db.problem-creation']} - {fields['performance.issues.m_n_plus_one_db.la-rollout']} - {fields['performance.issues.m_n_plus_one_db.ea-rollout']} - {fields['performance.issues.m_n_plus_one_db.ga-rollout']} - - - - Performance Issues - Consecutive HTTP Span Detector - - {fields['performance.issues.consecutive_http.max_duration_between_spans']} - {fields['performance.issues.consecutive_http.consecutive_count_threshold']} - {fields['performance.issues.consecutive_http.span_duration_threshold']} - - - Performance Issues - Large HTTP Payload Detector - {fields['performance.issues.large_http_payload.size_threshold']} - - - - Profiling Issues - Block Main Thread Detector Ingest - - {fields['profile.issues.blocked_main_thread-ingest.la-rollout']} - {fields['profile.issues.blocked_main_thread-ingest.ea-rollout']} - {fields['profile.issues.blocked_main_thread-ingest.ga-rollout']} - - - - Profiling Issues - Block Main Thread Detector Post Process Group - - {fields['profile.issues.blocked_main_thread-ppg.la-rollout']} - {fields['profile.issues.blocked_main_thread-ppg.ea-rollout']} - {fields['profile.issues.blocked_main_thread-ppg.ga-rollout']} - - - - - View Hierarchy - - - -
- ); - } + + {t('Performance Issues - N+1 API Calls Detector')} + {fields['performance.issues.n_plus_one_api_calls.problem-creation']} + {fields['performance.issues.n_plus_one_api_calls.la-rollout']} + {fields['performance.issues.n_plus_one_api_calls.ea-rollout']} + {fields['performance.issues.n_plus_one_api_calls.ga-rollout']} + + + + {t('Performance Issues - Compressed Assets Detector')} + + {fields['performance.issues.compressed_assets.problem-creation']} + {fields['performance.issues.compressed_assets.la-rollout']} + {fields['performance.issues.compressed_assets.ea-rollout']} + {fields['performance.issues.compressed_assets.ga-rollout']} + + + {t('Performance Issues - File IO on Main Thread')} + {fields['performance.issues.file_io_main_thread.problem-creation']} + + + {t('Performance Issues - Slow DB Span Detector')} + {fields['performance.issues.slow_db_query.problem-creation']} + {fields['performance.issues.slow_db_query.la-rollout']} + {fields['performance.issues.slow_db_query.ea-rollout']} + {fields['performance.issues.slow_db_query.ga-rollout']} + + + + {t('Performance Issues - Large Render Blocking Asset Detector')} + + {fields['performance.issues.render_blocking_assets.problem-creation']} + {fields['performance.issues.render_blocking_assets.la-rollout']} + {fields['performance.issues.render_blocking_assets.ea-rollout']} + {fields['performance.issues.render_blocking_assets.ga-rollout']} + + + {t('Performance Issues - MN+1 DB Detector')} + {fields['performance.issues.m_n_plus_one_db.problem-creation']} + {fields['performance.issues.m_n_plus_one_db.la-rollout']} + {fields['performance.issues.m_n_plus_one_db.ea-rollout']} + {fields['performance.issues.m_n_plus_one_db.ga-rollout']} + + + + {t('Performance Issues - Consecutive HTTP Span Detector')} + + {fields['performance.issues.consecutive_http.max_duration_between_spans']} + {fields['performance.issues.consecutive_http.consecutive_count_threshold']} + {fields['performance.issues.consecutive_http.span_duration_threshold']} + + + + {t('Performance Issues - Large HTTP Payload Detector')} + + {fields['performance.issues.large_http_payload.size_threshold']} + + + + {t('Profiling Issues - Block Main Thread Detector Ingest')} + + {fields['profile.issues.blocked_main_thread-ingest.la-rollout']} + {fields['profile.issues.blocked_main_thread-ingest.ea-rollout']} + {fields['profile.issues.blocked_main_thread-ingest.ga-rollout']} + + + + {t('Profiling Issues - Block Main Thread Detector Post Process Group')} + + {fields['profile.issues.blocked_main_thread-ppg.la-rollout']} + {fields['profile.issues.blocked_main_thread-ppg.ea-rollout']} + {fields['profile.issues.blocked_main_thread-ppg.ga-rollout']} + + + + + {t('View Hierarchy')} + + + +
+ ); } From 80fae32d5e44c51a24011af9043f6c9a7fb24bfd Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Mon, 13 May 2024 15:17:03 -0400 Subject: [PATCH 14/31] fix(trace-explorer): Merge parallel spans (#70681) Parallel spans can create a deeply nested timeline. This tries to merge them together for a cleaner flatten visualization. We're explicitly not merging root spans as that can create strange visualizations. --- .../api/endpoints/organization_traces.py | 35 ++- .../api/endpoints/test_organization_traces.py | 220 ++++++++++++++++++ 2 files changed, 244 insertions(+), 11 deletions(-) diff --git a/src/sentry/api/endpoints/organization_traces.py b/src/sentry/api/endpoints/organization_traces.py index ea3518afe78d82..42e2c0b8e8c5d9 100644 --- a/src/sentry/api/endpoints/organization_traces.py +++ b/src/sentry/api/endpoints/organization_traces.py @@ -41,6 +41,7 @@ class TraceInterval(TypedDict): kind: Literal["project", "missing", "other"] opCategory: str | None duration: int + isRoot: bool components: NotRequired[list[tuple[int, int]]] @@ -722,7 +723,7 @@ def get_traces_breakdown_projects_query( "precise.start_ts", "precise.finish_ts", ], - orderby=["precise.start_ts", "precise.finish_ts"], + orderby=["precise.start_ts", "-precise.finish_ts"], # limit the number of segments we fetch per trace so a single # large trace does not result in the rest being blank limitby=("trace", int(MAX_SNUBA_RESULTS / len(trace_ids))), @@ -760,10 +761,11 @@ def get_traces_breakdown_categories_query( "transaction", "span.category", "sdk.name", + "parent_span", "precise.start_ts", "precise.finish_ts", ], - orderby=["precise.start_ts", "precise.finish_ts"], + orderby=["precise.start_ts", "-precise.finish_ts"], # limit the number of segments we fetch per trace so a single # large trace does not result in the rest being blank limitby=("trace", int(MAX_SNUBA_RESULTS / len(trace_ids))), @@ -1000,7 +1002,12 @@ def process_breakdowns(data, traces_range): def should_merge(interval_a, interval_b): return ( - interval_a["end"] >= interval_b["start"] + # only merge intervals that have parent spans, i.e. those that aren't the trace root + not interval_a["isRoot"] + and not interval_b["isRoot"] + # only merge intervals that overlap + and interval_a["end"] >= interval_b["start"] + # only merge intervals that are part of the same service and interval_a["project"] == interval_b["project"] and interval_a["sdkName"] == interval_b["sdkName"] and interval_a["opCategory"] == interval_b["opCategory"] @@ -1032,14 +1039,16 @@ def breakdown_push(trace, interval): "components": [ (last_interval["components"][-1][1], interval["components"][0][0]), ], + "isRoot": False, } ) breakdown.append(interval) def stack_push(trace, interval): - last_interval = stack_peek(trace) - if last_interval and should_merge(last_interval, interval): + for last_interval in reversed(stacks[trace]): + if not should_merge(last_interval, interval): + continue # update the end of this interval and it will # be updated in the breakdown as well last_interval["end"] = max(interval["end"], last_interval["end"]) @@ -1107,7 +1116,14 @@ def stack_clear(trace, until=None): row["quantized.start_ts"] = quantized_start row["quantized.finish_ts"] = quantized_end - data.sort(key=lambda row: (row["quantized.start_ts"], -row["quantized.finish_ts"])) + data.sort( + key=lambda row: ( + row["quantized.start_ts"], + row["precise.start_ts"], + -row["quantized.finish_ts"], + -row["precise.finish_ts"], + ) + ) last_timestamp_per_trace: dict[str, int] = defaultdict(int) @@ -1131,6 +1147,7 @@ def stack_clear(trace, until=None): "end": row["quantized.finish_ts"], "duration": 0, "components": [(row["precise.start_ts"], row["precise.finish_ts"])], + "isRoot": not bool(row.get("parent_span")), } # Clear the stack of any intervals that end before the current interval @@ -1139,11 +1156,6 @@ def stack_clear(trace, until=None): stack_push(trace, cur) - # Clear the stack of any intervals that end before the current interval - # ends. Here we do not need to push them to the breakdowns because - # that time has already be attributed to the most recent interval. - stack_clear(trace, until=cur["end"]) - for trace, trace_range in traces_range.items(): # Check to see if there is still a gap before the trace ends and fill it # with an other interval. @@ -1158,6 +1170,7 @@ def stack_clear(trace, until=None): "start": other_start, "end": other_end, "duration": 0, + "isRoot": False, } # Clear the remaining intervals on the stack to find the latest end time diff --git a/tests/sentry/api/endpoints/test_organization_traces.py b/tests/sentry/api/endpoints/test_organization_traces.py index 3c63bddc49eb15..0cf9755d89ade4 100644 --- a/tests/sentry/api/endpoints/test_organization_traces.py +++ b/tests/sentry/api/endpoints/test_organization_traces.py @@ -458,6 +458,7 @@ def test_matching_tag(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[0].timestamp() * 1000), "end": int(timestamps[0].timestamp() * 1000) + 60_100, "kind": "project", @@ -467,6 +468,7 @@ def test_matching_tag(self): "project": project_2.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[1].timestamp() * 1000), "end": int(timestamps[3].timestamp() * 1000) + 30_003, "kind": "project", @@ -513,6 +515,7 @@ def test_matching_tag(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[4].timestamp() * 1000), "end": int(timestamps[4].timestamp() * 1000) + 90_123, "kind": "project", @@ -522,6 +525,7 @@ def test_matching_tag(self): "project": project_2.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[5].timestamp() * 1000), "end": int(timestamps[6].timestamp() * 1000) + 20_006, "kind": "project", @@ -611,6 +615,7 @@ def test_matching_tag_breakdown_with_category(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[4].timestamp() * 1000), "end": int(timestamps[4].timestamp() * 1000) + 90_123, "kind": "project", @@ -620,6 +625,7 @@ def test_matching_tag_breakdown_with_category(self): "project": project_1.slug, "opCategory": "http", "sdkName": "", + "isRoot": False, "start": int(timestamps[7].timestamp() * 1000), "end": int(timestamps[7].timestamp() * 1000) + 1_000, "kind": "project", @@ -629,6 +635,7 @@ def test_matching_tag_breakdown_with_category(self): "project": project_2.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[5].timestamp() * 1000), "end": int(timestamps[6].timestamp() * 1000) + 20_006, "kind": "project", @@ -638,6 +645,7 @@ def test_matching_tag_breakdown_with_category(self): "project": project_1.slug, "opCategory": "db", "sdkName": "", + "isRoot": False, "start": int(timestamps[8].timestamp() * 1000), "end": int(timestamps[8].timestamp() * 1000) + 3_000, "kind": "project", @@ -723,6 +731,7 @@ def test_matching_tag_metrics(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.remix", + "isRoot": False, "start": int(timestamps[10].timestamp() * 1000), "end": int(timestamps[10].timestamp() * 1000) + 40_000, "kind": "project", @@ -732,6 +741,7 @@ def test_matching_tag_metrics(self): "project": project_1.slug, "opCategory": None, "sdkName": "sentry.javascript.node", + "isRoot": False, "start": int(timestamps[11].timestamp() * 1000), "end": int(timestamps[11].timestamp() * 1000) + 10_000, "kind": "project", @@ -808,6 +818,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -825,6 +836,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, ], }, @@ -836,6 +848,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -844,6 +857,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.025, "precise.finish_ts": 0.075, @@ -861,6 +875,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, { "project": "bar", @@ -870,6 +885,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 50, + "isRoot": False, }, ], }, @@ -881,6 +897,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -889,6 +906,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.025, "precise.finish_ts": 0.075, @@ -897,6 +915,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "baz", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "baz1", "precise.start_ts": 0.05, "precise.finish_ts": 0.1, @@ -914,6 +933,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "bar", @@ -923,6 +943,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "baz", @@ -932,6 +953,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 50, + "isRoot": False, }, ], }, @@ -943,6 +965,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.025, @@ -951,6 +974,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.05, "precise.finish_ts": 0.075, @@ -968,6 +992,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 25, "kind": "project", "duration": 25, + "isRoot": False, }, { "project": None, @@ -977,6 +1002,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "missing", "duration": 25, + "isRoot": False, }, { "project": "bar", @@ -986,6 +1012,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 25, + "isRoot": False, }, ], }, @@ -997,6 +1024,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1005,6 +1033,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo2", "precise.start_ts": 0.025, "precise.finish_ts": 0.075, @@ -1022,6 +1051,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, ], }, @@ -1033,6 +1063,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.075, @@ -1041,6 +1072,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo2", "precise.start_ts": 0.025, "precise.finish_ts": 0.1, @@ -1058,6 +1090,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, ], }, @@ -1069,6 +1102,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.025, @@ -1077,6 +1111,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo2", "precise.start_ts": 0.05, "precise.finish_ts": 0.075, @@ -1094,6 +1129,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 25, "kind": "project", "duration": 25, + "isRoot": False, }, { "project": None, @@ -1103,6 +1139,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "missing", "duration": 25, + "isRoot": False, }, { "project": "foo", @@ -1112,6 +1149,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 25, + "isRoot": False, }, ], }, @@ -1123,6 +1161,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1131,6 +1170,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.02, "precise.finish_ts": 0.08, @@ -1139,6 +1179,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "baz", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "baz1", "precise.start_ts": 0.04, "precise.finish_ts": 0.06, @@ -1156,6 +1197,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, { "project": "bar", @@ -1165,6 +1207,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 80, "kind": "project", "duration": 60, + "isRoot": False, }, { "project": "baz", @@ -1174,6 +1217,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 60, "kind": "project", "duration": 20, + "isRoot": False, }, ], }, @@ -1185,6 +1229,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1193,6 +1238,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.025, "precise.finish_ts": 0.05, @@ -1200,6 +1246,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): { "trace": "a" * 32, "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "project": "baz", "transaction": "baz1", "precise.start_ts": 0.05, @@ -1218,6 +1265,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, { "project": "bar", @@ -1227,6 +1275,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 25, + "isRoot": False, }, { "project": "baz", @@ -1236,6 +1285,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 25, + "isRoot": False, }, ], }, @@ -1247,6 +1297,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -1255,6 +1306,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.02, "precise.finish_ts": 0.03, @@ -1263,6 +1315,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "baz", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "baz1", "precise.start_ts": 0.05, "precise.finish_ts": 0.075, @@ -1280,6 +1333,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "bar", @@ -1289,6 +1343,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 30, "kind": "project", "duration": 10, + "isRoot": False, }, { "project": "baz", @@ -1298,6 +1353,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 75, "kind": "project", "duration": 25, + "isRoot": False, }, ], }, @@ -1309,6 +1365,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -1317,6 +1374,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.02, "precise.finish_ts": 0.03, @@ -1325,6 +1383,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "baz", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "baz1", "precise.start_ts": 0.04, "precise.finish_ts": 0.06, @@ -1342,6 +1401,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "bar", @@ -1351,6 +1411,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 30, "kind": "project", "duration": 10, + "isRoot": False, }, { "project": "baz", @@ -1360,6 +1421,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 60, "kind": "project", "duration": 20, + "isRoot": False, }, ], }, @@ -1371,6 +1433,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -1379,6 +1442,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.01, "precise.finish_ts": 0.02, @@ -1387,6 +1451,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.03, "precise.finish_ts": 0.04, @@ -1404,6 +1469,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": "bar", @@ -1413,6 +1479,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 20, "kind": "project", "duration": 10, + "isRoot": False, }, ], }, @@ -1424,6 +1491,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1441,6 +1509,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 50, "kind": "project", "duration": 50, + "isRoot": False, }, ], }, @@ -1452,6 +1521,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.05, @@ -1469,6 +1539,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 40, "kind": "project", "duration": 50, + "isRoot": False, }, { "project": None, @@ -1478,6 +1549,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "other", "duration": 50, + "isRoot": False, }, ], }, @@ -1489,6 +1561,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.012, @@ -1497,6 +1570,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.013, "precise.finish_ts": 0.024, @@ -1505,6 +1579,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.032, "precise.finish_ts": 0.040, @@ -1522,6 +1597,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 20, "kind": "project", "duration": 23, + "isRoot": False, }, { "project": None, @@ -1531,6 +1607,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 30, "kind": "missing", "duration": 8, + "isRoot": False, }, { "project": "foo", @@ -1540,6 +1617,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 40, "kind": "project", "duration": 8, + "isRoot": False, }, ], }, @@ -1551,6 +1629,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.1, @@ -1559,6 +1638,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "bar", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "bar1", "precise.start_ts": 0.002, "precise.finish_ts": 0.044, @@ -1567,6 +1647,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.007, "precise.finish_ts": 0.1, @@ -1584,6 +1665,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 100, + "isRoot": False, }, { "project": "bar", @@ -1593,6 +1675,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 40, "kind": "project", "duration": 42, + "isRoot": False, }, ], }, @@ -1604,6 +1687,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0, "precise.finish_ts": 0.051, @@ -1612,6 +1696,7 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "trace": "a" * 32, "project": "foo", "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, "transaction": "foo1", "precise.start_ts": 0.069, "precise.finish_ts": 0.1, @@ -1629,11 +1714,146 @@ def test_matching_tag_metrics_but_no_matching_spans(self): "end": 100, "kind": "project", "duration": 82, + "isRoot": False, }, ], }, id="merges nearby spans", ), + pytest.param( + [ + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.remix", + "transaction": "foo1", + "precise.start_ts": 0, + "precise.finish_ts": 0.1, + }, + { + "trace": "a" * 32, + "project": "bar", + "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, + "transaction": "bar", + "precise.start_ts": 0.02, + "precise.finish_ts": 0.06, + }, + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.remix", + "parent_span": "a" * 16, + "transaction": "foo1", + "precise.start_ts": 0.03, + "precise.finish_ts": 0.07, + }, + { + "trace": "a" * 32, + "project": "bar", + "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, + "transaction": "bar1", + "precise.start_ts": 0.04, + "precise.finish_ts": 0.08, + }, + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.remix", + "parent_span": "a" * 16, + "transaction": "foo1", + "precise.start_ts": 0.05, + "precise.finish_ts": 0.07, + }, + ], + {"a" * 32: (0, 100, 10)}, + { + "a" + * 32: [ + { + "project": "foo", + "opCategory": None, + "sdkName": "sentry.javascript.remix", + "start": 0, + "end": 100, + "kind": "project", + "duration": 100, + "isRoot": True, + }, + { + "project": "bar", + "opCategory": None, + "sdkName": "sentry.javascript.node", + "start": 20, + "end": 80, + "kind": "project", + "duration": 60, + "isRoot": False, + }, + { + "project": "foo", + "opCategory": None, + "sdkName": "sentry.javascript.remix", + "start": 30, + "end": 70, + "kind": "project", + "duration": 40, + "isRoot": False, + }, + ], + }, + id="merges spans at different depths", + ), + pytest.param( + [ + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.node", + "parent_span": "a" * 16, + "transaction": "foo1", + "precise.start_ts": 0.003, + "precise.finish_ts": 0.097, + }, + { + "trace": "a" * 32, + "project": "foo", + "sdk.name": "sentry.javascript.remix", + "parent_span": "a" * 16, + "transaction": "foo1", + "precise.start_ts": 0.002, + "precise.finish_ts": 0.098, + }, + ], + {"a" * 32: (0, 100, 10)}, + { + "a" + * 32: [ + { + "project": "foo", + "opCategory": None, + "sdkName": "sentry.javascript.remix", + "start": 0, + "end": 100, + "kind": "project", + "duration": 96, + "isRoot": False, + }, + { + "project": "foo", + "opCategory": None, + "sdkName": "sentry.javascript.node", + "start": 0, + "end": 100, + "kind": "project", + "duration": 94, + "isRoot": False, + }, + ], + }, + id="orders spans by precise timestamps", + ), ], ) def test_process_breakdowns(data, traces_range, expected): From c81dd1a88be3bcb78f11404860b60d4dd44c3f1f Mon Sep 17 00:00:00 2001 From: Lyn Nagara <1779792+lynnagara@users.noreply.github.com> Date: Mon, 13 May 2024 12:17:45 -0700 Subject: [PATCH 15/31] test: requires_kafka if snuba subscriptions are created (#70478) required for https://github.com/getsentry/snuba/pull/5868 and https://github.com/getsentry/ops/pull/10392 --- tests/sentry/incidents/test_tasks.py | 4 ++-- tests/sentry/snuba/test_query_subscription_consumer.py | 4 ++-- tests/sentry/snuba/test_subscriptions.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/sentry/incidents/test_tasks.py b/tests/sentry/incidents/test_tasks.py index dcbc81d577676e..39bcb0b2345243 100644 --- a/tests/sentry/incidents/test_tasks.py +++ b/tests/sentry/incidents/test_tasks.py @@ -37,10 +37,10 @@ from sentry.snuba.subscriptions import create_snuba_query, create_snuba_subscription from sentry.testutils.cases import TestCase from sentry.testutils.helpers.datetime import freeze_time -from sentry.testutils.skips import requires_snuba +from sentry.testutils.skips import requires_kafka, requires_snuba from sentry.utils.http import absolute_uri -pytestmark = [pytest.mark.sentry_metrics, requires_snuba] +pytestmark = [pytest.mark.sentry_metrics, requires_snuba, requires_kafka] class BaseIncidentActivityTest(TestCase): diff --git a/tests/sentry/snuba/test_query_subscription_consumer.py b/tests/sentry/snuba/test_query_subscription_consumer.py index 7051e1713ebdbb..0f20e42bcdeeb1 100644 --- a/tests/sentry/snuba/test_query_subscription_consumer.py +++ b/tests/sentry/snuba/test_query_subscription_consumer.py @@ -24,10 +24,10 @@ from sentry.snuba.query_subscriptions.run import QuerySubscriptionStrategyFactory from sentry.snuba.subscriptions import create_snuba_query, create_snuba_subscription from sentry.testutils.cases import TestCase -from sentry.testutils.skips import requires_snuba +from sentry.testutils.skips import requires_kafka, requires_snuba from sentry.utils import json -pytestmark = [requires_snuba] +pytestmark = [requires_snuba, requires_kafka] @pytest.mark.snuba_ci diff --git a/tests/sentry/snuba/test_subscriptions.py b/tests/sentry/snuba/test_subscriptions.py index ab75ee7e587e54..df1e21aefd41c5 100644 --- a/tests/sentry/snuba/test_subscriptions.py +++ b/tests/sentry/snuba/test_subscriptions.py @@ -13,9 +13,9 @@ update_snuba_subscription, ) from sentry.testutils.cases import TestCase -from sentry.testutils.skips import requires_snuba +from sentry.testutils.skips import requires_kafka, requires_snuba -pytestmark = [pytest.mark.sentry_metrics, requires_snuba] +pytestmark = [pytest.mark.sentry_metrics, requires_snuba, requires_kafka] @pytest.mark.snuba_ci From 9ff054d1a35b6e1efe2856829bbc0bbd6b48ccd8 Mon Sep 17 00:00:00 2001 From: Colleen O'Rourke Date: Mon, 13 May 2024 12:26:57 -0700 Subject: [PATCH 16/31] ref(delayed rules): Add instrumentation (#70693) Add instrumentation and logging to the delayed rule processor to measure how long the bulkier functions are taking and how many rules and groups we're processing. Closes https://getsentry.atlassian.net/browse/ALRT-19 and https://github.com/getsentry/team-core-product-foundations/issues/308 (a dupe) as a follow up to https://github.com/getsentry/sentry/pull/69830#pullrequestreview-2036397916 --- .../rules/processing/delayed_processing.py | 66 ++++++++++--------- src/sentry/rules/processing/processor.py | 3 +- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/sentry/rules/processing/delayed_processing.py b/src/sentry/rules/processing/delayed_processing.py index 4cabe5086fe34d..136443124434a8 100644 --- a/src/sentry/rules/processing/delayed_processing.py +++ b/src/sentry/rules/processing/delayed_processing.py @@ -346,7 +346,8 @@ def apply_delayed(project_id: int, *args: Any, **kwargs: Any) -> None: condition_groups = get_condition_groups(alert_rules, rules_to_groups) # Step 5: Instantiate each unique condition, and evaluate the relevant # group_ids that apply for that condition - condition_group_results = get_condition_group_results(condition_groups, project) + with metrics.timer("delayed_processing.get_condition_group_results.duration"): + condition_group_results = get_condition_group_results(condition_groups, project) # Step 6: For each rule and group applying to that rule, check if the group # meets the conditions of the rule (basically doing BaseEventFrequencyCondition.passes) rule_to_slow_conditions = get_rule_to_slow_conditions(alert_rules) @@ -363,39 +364,42 @@ def apply_delayed(project_id: int, *args: Any, **kwargs: Any) -> None: now = datetime.now(tz=timezone.utc) parsed_rulegroup_to_event_data = parse_rulegroup_to_event_data(rulegroup_to_event_data) - for rule, group_ids in rules_to_fire.items(): - frequency = rule.data.get("frequency") or Rule.DEFAULT_FREQUENCY - freq_offset = now - timedelta(minutes=frequency) - group_to_groupevent = get_group_to_groupevent( - parsed_rulegroup_to_event_data, project.id, group_ids - ) - for group, groupevent in group_to_groupevent.items(): - rule_statuses = bulk_get_rule_status(alert_rules, group, project) - status = rule_statuses[rule.id] - if status.last_active and status.last_active > freq_offset: - logger.info( - "delayed_processing.last_active", - extra={"last_active": status.last_active, "freq_offset": freq_offset}, + with metrics.timer("delayed_processing.fire_rules.duration"): + for rule, group_ids in rules_to_fire.items(): + frequency = rule.data.get("frequency") or Rule.DEFAULT_FREQUENCY + freq_offset = now - timedelta(minutes=frequency) + group_to_groupevent = get_group_to_groupevent( + parsed_rulegroup_to_event_data, project.id, group_ids + ) + for group, groupevent in group_to_groupevent.items(): + rule_statuses = bulk_get_rule_status(alert_rules, group, project) + status = rule_statuses[rule.id] + if status.last_active and status.last_active > freq_offset: + logger.info( + "delayed_processing.last_active", + extra={"last_active": status.last_active, "freq_offset": freq_offset}, + ) + return + + updated = ( + GroupRuleStatus.objects.filter(id=status.id) + .exclude(last_active__gt=freq_offset) + .update(last_active=now) ) - return - updated = ( - GroupRuleStatus.objects.filter(id=status.id) - .exclude(last_active__gt=freq_offset) - .update(last_active=now) - ) + if not updated: + logger.info("delayed_processing.not_updated", extra={"status_id": status.id}) + return - if not updated: - logger.info("delayed_processing.not_updated", extra={"status_id": status.id}) - return - - notification_uuid = str(uuid.uuid4()) - groupevent = group_to_groupevent[group] - rule_fire_history = history.record(rule, group, groupevent.event_id, notification_uuid) - for callback, futures in activate_downstream_actions( - rule, groupevent, notification_uuid, rule_fire_history - ).values(): - safe_execute(callback, groupevent, futures, _with_transaction=False) + notification_uuid = str(uuid.uuid4()) + groupevent = group_to_groupevent[group] + rule_fire_history = history.record( + rule, group, groupevent.event_id, notification_uuid + ) + for callback, futures in activate_downstream_actions( + rule, groupevent, notification_uuid, rule_fire_history + ).values(): + safe_execute(callback, groupevent, futures, _with_transaction=False) # Step 8: Clean up Redis buffer data hashes_to_delete = [ diff --git a/src/sentry/rules/processing/processor.py b/src/sentry/rules/processing/processor.py index bbb96f1fd716ea..961cc03be31f64 100644 --- a/src/sentry/rules/processing/processor.py +++ b/src/sentry/rules/processing/processor.py @@ -26,7 +26,7 @@ from sentry.rules.conditions.event_frequency import EventFrequencyConditionData from sentry.rules.filters.base import EventFilter from sentry.types.rules import RuleFuture -from sentry.utils import json +from sentry.utils import json, metrics from sentry.utils.hashlib import hash_values from sentry.utils.safe import safe_execute @@ -275,6 +275,7 @@ def enqueue_rule(self, rule: Rule) -> None: field=f"{rule.id}:{self.group.id}", value=value, ) + metrics.incr("delayed_rule.group_added") def apply_rule(self, rule: Rule, status: GroupRuleStatus) -> None: """ From 682c237c5c2ff6b3311fa5ff7325880426a129bb Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Mon, 13 May 2024 16:19:01 -0400 Subject: [PATCH 17/31] chore(trace-explorer): Start passing breakdown slices to traces endpoint (#70804) Trying to refactor this endpoint. Going to be passing an integer number of slices instead of a floating point percentage of the trace for simplicity. The backend will quietly move to using this integer. --- static/app/views/performance/traces/content.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/views/performance/traces/content.tsx b/static/app/views/performance/traces/content.tsx index 7d9f7881c7b10c..946960a6341667 100644 --- a/static/app/views/performance/traces/content.tsx +++ b/static/app/views/performance/traces/content.tsx @@ -441,6 +441,7 @@ function useTraces({ suggestedQuery, sort, per_page: limit, + breakdownSlices: 40, minBreakdownPercentage: 1 / 40, maxSpansPerTrace: 5, mri, From 8f051f55a9dc5907cf2b7dc020ddb92071119613 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 13 May 2024 16:25:10 -0400 Subject: [PATCH 18/31] ref(replay): update feature name (#70801) For self-hosted users, replay details's Give Feedback button routes feedback to our `feedback` project on issues: https://github.com/getsentry/sentry/assets/56095982/30a3900a-e723-4e5e-a198-cb0cb9926ca5 This PR updates the feature name so that the title is `Replay Self-Hosted` for more clarity. (Does not affect the zendesk widget for SaaS users) SCR-20240513-lees SCR-20240513-leeh --- static/app/components/replays/header/feedbackButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/replays/header/feedbackButton.tsx b/static/app/components/replays/header/feedbackButton.tsx index e66b58da4d02bd..916432caf76caf 100644 --- a/static/app/components/replays/header/feedbackButton.tsx +++ b/static/app/components/replays/header/feedbackButton.tsx @@ -11,7 +11,7 @@ const FeedbackButtonHook = HookOrDefault({ function FeedbackButton() { return ( - + ); } From 1c5945c5ea60d64c1310c405e3f54839951b5127 Mon Sep 17 00:00:00 2001 From: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com> Date: Mon, 13 May 2024 16:38:04 -0400 Subject: [PATCH 19/31] ref(screenload): fix display of platformSelector and feedback button (#70803) Before: SCR-20240513-lksj After: SCR-20240513-loqh --- static/app/views/performance/mobile/screenload/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/app/views/performance/mobile/screenload/index.tsx b/static/app/views/performance/mobile/screenload/index.tsx index 1e0965a12c1502..c401f811d2112f 100644 --- a/static/app/views/performance/mobile/screenload/index.tsx +++ b/static/app/views/performance/mobile/screenload/index.tsx @@ -61,13 +61,13 @@ export default function PageloadModule() { /> {t('Screen Loads')} - {organization.features.includes('spans-first-ui') && - project && - isCrossPlatform(project) && } + {organization.features.includes('spans-first-ui') && + project && + isCrossPlatform(project) && } From 3b02cd0220c1f0c95c0a20a219dae9ae91e565de Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki <44422760+DominikB2014@users.noreply.github.com> Date: Mon, 13 May 2024 16:39:22 -0400 Subject: [PATCH 20/31] feat(insights): plot avg transaction duration on cache sidebar (#70808) Plot the average transaction duration over the time interval (dashed grey line) on the transaction duration graph image --- .../samplePanel/charts/transactionDurationChart.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/static/app/views/performance/cache/samplePanel/charts/transactionDurationChart.tsx b/static/app/views/performance/cache/samplePanel/charts/transactionDurationChart.tsx index d9ab18c3cb4de8..625d425bcabb20 100644 --- a/static/app/views/performance/cache/samplePanel/charts/transactionDurationChart.tsx +++ b/static/app/views/performance/cache/samplePanel/charts/transactionDurationChart.tsx @@ -1,11 +1,12 @@ import {t} from 'sentry/locale'; -import type {EChartHighlightHandler} from 'sentry/types/echarts'; +import type {EChartHighlightHandler, Series} from 'sentry/types/echarts'; import {decodeScalar} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import useLocationQuery from 'sentry/utils/url/useLocationQuery'; import {Referrer} from 'sentry/views/performance/cache/referrers'; import {CHART_HEIGHT} from 'sentry/views/performance/cache/settings'; import type {DataRow} from 'sentry/views/performance/cache/tables/spanSamplesTable'; +import {AverageValueMarkLine} from 'sentry/views/performance/charts/averageValueMarkLine'; import {AVG_COLOR} from 'sentry/views/starfish/colors'; import Chart, {ChartType} from 'sentry/views/starfish/components/chart'; import ChartPanel from 'sentry/views/starfish/components/chartPanel'; @@ -73,6 +74,14 @@ export function TransactionDurationChart({ onHighlight?.(highlightedDataPoints, event); }; + const baselineAvgSeries: Series = { + seriesName: 'Average', + data: [], + markLine: AverageValueMarkLine({ + value: averageTransactionDuration, + }), + }; + return ( Date: Mon, 13 May 2024 16:50:21 -0400 Subject: [PATCH 21/31] fix(feedback): move call for get_latest_event (#70807) `get_latest_event` should be called for every `item` in `item_list` --- src/sentry/api/serializers/models/group_stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/api/serializers/models/group_stream.py b/src/sentry/api/serializers/models/group_stream.py index c80ba5c2ed225d..73db5721329557 100644 --- a/src/sentry/api/serializers/models/group_stream.py +++ b/src/sentry/api/serializers/models/group_stream.py @@ -405,8 +405,8 @@ def get_attrs( ): return self.respond(status=404) - latest_event = item.get_latest_event() for item in item_list: + latest_event = item.get_latest_event() num_attachments = EventAttachment.objects.filter( project_id=latest_event.project_id, event_id=latest_event.event_id ).count() From b8eafa515286c15760757edce8ccf18191d781ef Mon Sep 17 00:00:00 2001 From: Josh Ferge Date: Mon, 13 May 2024 14:06:22 -0700 Subject: [PATCH 22/31] fix(feedback): correct issue data type and add substatus (#70802) - in evidence_data, is_spam should be a boolean, not a string - the status change message needs a substatus. didn't see these logs: - https://cloudlogging.app.goo.gl/ZgZWLu2UqNjdtpBA7 --- .../feedback/usecases/create_feedback.py | 5 +- .../feedback/usecases/test_create_feedback.py | 87 ++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/src/sentry/feedback/usecases/create_feedback.py b/src/sentry/feedback/usecases/create_feedback.py index 74d3b5d64cf788..698dc54267fb9e 100644 --- a/src/sentry/feedback/usecases/create_feedback.py +++ b/src/sentry/feedback/usecases/create_feedback.py @@ -20,6 +20,7 @@ from sentry.models.group import GroupStatus from sentry.models.project import Project from sentry.signals import first_feedback_received, first_new_feedback_received +from sentry.types.group import GroupSubStatus from sentry.utils import metrics from sentry.utils.outcomes import Outcome, track_outcome from sentry.utils.safe import get_path @@ -88,7 +89,7 @@ def make_evidence(feedback, source: FeedbackCreationSource, is_message_spam: boo evidence_display.append(IssueEvidence(name="source", value=source.value, important=False)) if is_message_spam is True: - evidence_data["is_spam"] = str(is_message_spam) + evidence_data["is_spam"] = is_message_spam evidence_display.append( IssueEvidence(name="is_spam", value=str(is_message_spam), important=False) ) @@ -360,6 +361,6 @@ def auto_ignore_spam_feedbacks(project, issue_fingerprint): fingerprint=issue_fingerprint, project_id=project.id, new_status=GroupStatus.IGNORED, # we use ignored in the UI for the spam tab - new_substatus=None, + new_substatus=GroupSubStatus.FOREVER, ), ) diff --git a/tests/sentry/feedback/usecases/test_create_feedback.py b/tests/sentry/feedback/usecases/test_create_feedback.py index 006f7950f0733d..585fc9d50ed917 100644 --- a/tests/sentry/feedback/usecases/test_create_feedback.py +++ b/tests/sentry/feedback/usecases/test_create_feedback.py @@ -14,9 +14,10 @@ fix_for_issue_platform, validate_issue_platform_event_schema, ) -from sentry.models.group import GroupStatus +from sentry.models.group import Group, GroupStatus from sentry.testutils.helpers import Feature from sentry.testutils.pytest.fixtures import django_db_all +from sentry.types.group import GroupSubStatus @pytest.fixture @@ -688,3 +689,87 @@ def test_create_feedback_adds_associated_event_id( ] associated_event_id = associated_event_id_evidence[0] if associated_event_id_evidence else None assert associated_event_id == "56b08cf7852c42cbb95e4a6998c66ad6" + + +@django_db_all +def test_create_feedback_spam_detection_adds_field_calls( + default_project, + monkeypatch, +): + with Feature( + { + "organizations:user-feedback-spam-filter-actions": True, + "organizations:user-feedback-spam-filter-ingest": True, + "organizations:issue-platform": True, + "organizations:feedback-ingest": True, + "organizations:feedback-post-process-group": True, + } + ): + event = { + "project_id": default_project.id, + "request": { + "url": "https://sentry.sentry.io/feedback/?statsPeriod=14d", + "headers": { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" + }, + }, + "event_id": "56b08cf7852c42cbb95e4a6998c66ad6", + "timestamp": 1698255009.574, + "received": "2021-10-24T22:23:29.574000+00:00", + "environment": "prod", + "release": "frontend@daf1316f209d961443664cd6eb4231ca154db502", + "user": { + "ip_address": "72.164.175.154", + "email": "josh.ferge@sentry.io", + "id": 880461, + "isStaff": False, + "name": "Josh Ferge", + }, + "contexts": { + "feedback": { + "contact_email": "josh.ferge@sentry.io", + "name": "Josh Ferge", + "message": "This is definitely spam", + "replay_id": "3d621c61593c4ff9b43f8490a78ae18e", + "url": "https://sentry.sentry.io/feedback/?statsPeriod=14d", + }, + }, + "breadcrumbs": [], + "platform": "javascript", + } + + def dummy_response(*args, **kwargs): + return ChatCompletion( + id="test", + choices=[ + Choice( + index=0, + message=ChatCompletionMessage( + content=( + "spam" + if "This is definitely spam" in kwargs["messages"][0]["content"] + else "not spam" + ), + role="assistant", + ), + finish_reason="stop", + ) + ], + created=time.time(), + model="gpt3.5-trubo", + object="chat.completion", + ) + + mock_openai = Mock() + mock_openai().chat.completions.create = dummy_response + + monkeypatch.setattr("sentry.llm.providers.openai.OpenAI", mock_openai) + + create_feedback_issue( + event, default_project.id, FeedbackCreationSource.NEW_FEEDBACK_ENVELOPE + ) + + assert Group.objects.all().count() == 1 + group = Group.objects.first() + assert group.status == GroupStatus.IGNORED + assert group.substatus == GroupSubStatus.FOREVER From 7c08bcb3d96aa9e6ccb3396a5cadb7cd0f94eb49 Mon Sep 17 00:00:00 2001 From: Matt Duncan <14761+mrduncan@users.noreply.github.com> Date: Mon, 13 May 2024 14:29:11 -0700 Subject: [PATCH 23/31] chore(hc): Enable stronger typing for a few hybridcloud modules (#70777) `sentry.hybridcloud.*` and `sentry.services.hybrid_cloud.*` aren't quite passing but we can enable these which already are in the mean time. --- pyproject.toml | 31 +++++++++++++++++++ .../control_organization_provisioning/impl.py | 4 +-- .../region_organization_provisioning/impl.py | 2 +- src/sentry/services/hybrid_cloud/app/impl.py | 2 +- .../hybrid_cloud/notifications/impl.py | 2 +- .../hybrid_cloud/organization_mapping/impl.py | 2 +- .../hybrid_cloud/project_key/model.py | 2 +- .../services/hybrid_cloud/replica/impl.py | 2 +- src/sentry/services/hybrid_cloud/rpc.py | 2 +- .../services/hybrid_cloud/rpcmetrics.py | 2 +- .../services/hybrid_cloud/user/model.py | 2 +- .../hybrid_cloud/organization/test_service.py | 12 +++---- .../hybrid_cloud/test_hybrid_cloud.py | 6 ++-- .../services/hybrid_cloud/test_rpcmetrics.py | 4 +-- .../services/hybrid_cloud/user/test_impl.py | 16 +++++----- 15 files changed, 61 insertions(+), 30 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 694a2250a6de57..fce15e3c2c596a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -558,6 +558,10 @@ module = [ "sentry.buffer.redis", "sentry.build.*", "sentry.eventstore.reprocessing.redis", + "sentry.hybridcloud", + "sentry.hybridcloud.migrations.*", + "sentry.hybridcloud.options", + "sentry.hybridcloud.rpc_services.*", "sentry.issues", "sentry.issues.analytics", "sentry.issues.apps", @@ -602,6 +606,32 @@ module = [ "sentry.relay.config.metric_extraction", "sentry.reprocessing2", "sentry.runner.*", + "sentry.services.hybrid_cloud.access.*", + "sentry.services.hybrid_cloud.app.*", + "sentry.services.hybrid_cloud.hook.*", + "sentry.services.hybrid_cloud.identity.*", + "sentry.services.hybrid_cloud.integration.*", + "sentry.services.hybrid_cloud.issue.*", + "sentry.services.hybrid_cloud.log.*", + "sentry.services.hybrid_cloud.lost_password_hash.*", + "sentry.services.hybrid_cloud.notifications.*", + "sentry.services.hybrid_cloud.organization_actions.*", + "sentry.services.hybrid_cloud.organization_mapping.*", + "sentry.services.hybrid_cloud.organization_provisioning.*", + "sentry.services.hybrid_cloud.organizationmember_mapping.*", + "sentry.services.hybrid_cloud.orgauthtoken.*", + "sentry.services.hybrid_cloud.pagination", + "sentry.services.hybrid_cloud.project.*", + "sentry.services.hybrid_cloud.project_key.*", + "sentry.services.hybrid_cloud.region", + "sentry.services.hybrid_cloud.replica.*", + "sentry.services.hybrid_cloud.repository.*", + "sentry.services.hybrid_cloud.rpcmetrics", + "sentry.services.hybrid_cloud.sig", + "sentry.services.hybrid_cloud.tombstone.*", + "sentry.services.hybrid_cloud.user.*", + "sentry.services.hybrid_cloud.user_option.*", + "sentry.services.hybrid_cloud.util", "sentry.snuba.metrics.extraction", "sentry.tasks.commit_context", "sentry.tasks.on_demand_metrics", @@ -667,6 +697,7 @@ module = [ "tests.sentry.issues.test_status_change_consumer", "tests.sentry.issues.test_update_inbox", "tests.sentry.relay.config.test_metric_extraction", + "tests.sentry.services.hybrid_cloud.*", "tests.sentry.tasks.test_on_demand_metrics", "tools.*", ] diff --git a/src/sentry/hybridcloud/rpc_services/control_organization_provisioning/impl.py b/src/sentry/hybridcloud/rpc_services/control_organization_provisioning/impl.py index 4a083e96a0560b..37556f331275d7 100644 --- a/src/sentry/hybridcloud/rpc_services/control_organization_provisioning/impl.py +++ b/src/sentry/hybridcloud/rpc_services/control_organization_provisioning/impl.py @@ -35,7 +35,7 @@ class SlugMismatchException(Exception): def create_post_provision_outbox( provisioning_options: OrganizationProvisioningOptions, org_id: int -): +) -> RegionOutbox: return RegionOutbox( shard_scope=OutboxScope.ORGANIZATION_SCOPE, shard_identifier=org_id, @@ -49,7 +49,7 @@ def create_organization_provisioning_outbox( organization_id: int, region_name: str, org_provision_payload: OrganizationProvisioningOptions | None, -): +) -> ControlOutbox: payload = org_provision_payload.json() if org_provision_payload is not None else None return ControlOutbox( region_name=region_name, diff --git a/src/sentry/hybridcloud/rpc_services/region_organization_provisioning/impl.py b/src/sentry/hybridcloud/rpc_services/region_organization_provisioning/impl.py index fdf9848bd72fcc..505e1eb8d86a74 100644 --- a/src/sentry/hybridcloud/rpc_services/region_organization_provisioning/impl.py +++ b/src/sentry/hybridcloud/rpc_services/region_organization_provisioning/impl.py @@ -20,7 +20,7 @@ def create_post_provision_outbox( provisioning_options: OrganizationProvisioningOptions, org_id: int -): +) -> RegionOutbox: return RegionOutbox( shard_scope=OutboxScope.ORGANIZATION_SCOPE, shard_identifier=org_id, diff --git a/src/sentry/services/hybrid_cloud/app/impl.py b/src/sentry/services/hybrid_cloud/app/impl.py index a73e4b5a7ded5f..5d3be4e6875c43 100644 --- a/src/sentry/services/hybrid_cloud/app/impl.py +++ b/src/sentry/services/hybrid_cloud/app/impl.py @@ -271,7 +271,7 @@ def create_internal_integration_for_channel_request( organization_id: int, integration_name: str, integration_scopes: list[str], - integration_creator_id, + integration_creator_id: int, metadata: dict[str, Any] | None = None, ) -> RpcSentryAppInstallation: admin_user = User.objects.get(id=integration_creator_id) diff --git a/src/sentry/services/hybrid_cloud/notifications/impl.py b/src/sentry/services/hybrid_cloud/notifications/impl.py index 144a261a7bd7c0..4fef88e4a5694c 100644 --- a/src/sentry/services/hybrid_cloud/notifications/impl.py +++ b/src/sentry/services/hybrid_cloud/notifications/impl.py @@ -64,7 +64,7 @@ def update_notification_options( scope_type: NotificationScopeEnum, scope_identifier: int, value: NotificationSettingsOptionEnum, - ): + ) -> None: kwargs = {} if actor.is_user: kwargs["user_id"] = actor.id diff --git a/src/sentry/services/hybrid_cloud/organization_mapping/impl.py b/src/sentry/services/hybrid_cloud/organization_mapping/impl.py index 3e17daeba19bae..c6a0c38382ac45 100644 --- a/src/sentry/services/hybrid_cloud/organization_mapping/impl.py +++ b/src/sentry/services/hybrid_cloud/organization_mapping/impl.py @@ -86,7 +86,7 @@ def _check_organization_mapping_integrity( def _upsert_organization_slug_reservation_for_monolith( self, organization_id: int, mapping_update: RpcOrganizationMappingUpdate - ): + ) -> None: org_slug_reservation_qs = OrganizationSlugReservation.objects.filter( organization_id=organization_id ) diff --git a/src/sentry/services/hybrid_cloud/project_key/model.py b/src/sentry/services/hybrid_cloud/project_key/model.py index c02f1ff137b7ac..3fed82bd348698 100644 --- a/src/sentry/services/hybrid_cloud/project_key/model.py +++ b/src/sentry/services/hybrid_cloud/project_key/model.py @@ -31,5 +31,5 @@ class RpcProjectKey(RpcModel): status: int = ProjectKeyStatus.INACTIVE @property - def is_active(self): + def is_active(self) -> bool: return self.status == ProjectKeyStatus.ACTIVE diff --git a/src/sentry/services/hybrid_cloud/replica/impl.py b/src/sentry/services/hybrid_cloud/replica/impl.py index d377ba5a628e18..c99e20b9d8fb43 100644 --- a/src/sentry/services/hybrid_cloud/replica/impl.py +++ b/src/sentry/services/hybrid_cloud/replica/impl.py @@ -123,7 +123,7 @@ def handle_replication( source_model: type[ReplicatedControlModel] | type[ReplicatedRegionModel], destination: BaseModel, fk: str | None = None, -): +) -> None: category: OutboxCategory = source_model.category destination_model: type[BaseModel] = type(destination) fk = fk or get_foreign_key_column(destination, source_model) diff --git a/src/sentry/services/hybrid_cloud/rpc.py b/src/sentry/services/hybrid_cloud/rpc.py index 97f9ca5450ff35..2c5e06d865c587 100644 --- a/src/sentry/services/hybrid_cloud/rpc.py +++ b/src/sentry/services/hybrid_cloud/rpc.py @@ -580,7 +580,7 @@ def _fire_request(self, headers: MutableMapping[str, str], data: bytes) -> reque except requests.exceptions.Timeout as e: raise self._remote_exception(f"Timeout of {settings.RPC_TIMEOUT} exceeded") from e - def _check_disabled(self): + def _check_disabled(self) -> None: if disabled_service_methods := options.get("hybrid_cloud.rpc.disabled-service-methods"): service_method = f"{self.service_name}.{self.method_name}" if service_method in disabled_service_methods: diff --git a/src/sentry/services/hybrid_cloud/rpcmetrics.py b/src/sentry/services/hybrid_cloud/rpcmetrics.py index 0b1c83d397f169..ae5533b1bf9931 100644 --- a/src/sentry/services/hybrid_cloud/rpcmetrics.py +++ b/src/sentry/services/hybrid_cloud/rpcmetrics.py @@ -52,7 +52,7 @@ def get_local(cls) -> RpcMetricTracker: new_tracker = _LOCAL_TRACKER.tracker = cls() return new_tracker - def save_record(self, record: RpcMetricRecord): + def save_record(self, record: RpcMetricRecord) -> None: for span in self.spans: span.records.append(record) diff --git a/src/sentry/services/hybrid_cloud/user/model.py b/src/sentry/services/hybrid_cloud/user/model.py index 4ce263f1b54d50..8d53b3d7b10800 100644 --- a/src/sentry/services/hybrid_cloud/user/model.py +++ b/src/sentry/services/hybrid_cloud/user/model.py @@ -71,7 +71,7 @@ def __hash__(self) -> int: # TODO: Remove the need for this return hash((self.id, self.pk)) - def __str__(self): # API compatibility with ORM User + def __str__(self) -> str: # API compatibility with ORM User return self.get_username() def by_email(self, email: str) -> "RpcUser": diff --git a/tests/sentry/services/hybrid_cloud/organization/test_service.py b/tests/sentry/services/hybrid_cloud/organization/test_service.py index 8113f285a0e5fe..87c264f1092adb 100644 --- a/tests/sentry/services/hybrid_cloud/organization/test_service.py +++ b/tests/sentry/services/hybrid_cloud/organization/test_service.py @@ -6,7 +6,7 @@ @all_silo_test class CheckOrganizationTest(TestCase): - def test_check_active_organization_by_slug(self): + def test_check_active_organization_by_slug(self) -> None: self.organization = self.create_organization(slug="test") assert ( organization_service.check_organization_by_slug(slug="test", only_visible=True) @@ -17,7 +17,7 @@ def test_check_active_organization_by_slug(self): == self.organization.id ) - def test_check_missing_organization_by_slug(self): + def test_check_missing_organization_by_slug(self) -> None: assert ( organization_service.check_organization_by_slug(slug="test", only_visible=True) is None ) @@ -25,7 +25,7 @@ def test_check_missing_organization_by_slug(self): organization_service.check_organization_by_slug(slug="test", only_visible=False) is None ) - def test_check_pending_deletion_organization_by_slug(self): + def test_check_pending_deletion_organization_by_slug(self) -> None: self.organization = self.create_organization(slug="test") self.organization.status = OrganizationStatus.PENDING_DELETION with assume_test_silo_mode_of(Organization): @@ -38,7 +38,7 @@ def test_check_pending_deletion_organization_by_slug(self): == self.organization.id ) - def test_check_active_organization_by_id(self): + def test_check_active_organization_by_id(self) -> None: organization = self.create_organization(slug="test") assert ( organization_service.check_organization_by_id(id=organization.id, only_visible=True) @@ -49,11 +49,11 @@ def test_check_active_organization_by_id(self): is True ) - def test_check_missing_organization_by_id(self): + def test_check_missing_organization_by_id(self) -> None: assert organization_service.check_organization_by_id(id=1234, only_visible=True) is False assert organization_service.check_organization_by_id(id=1234, only_visible=False) is False - def test_check_pending_deletion_organization_by_id(self): + def test_check_pending_deletion_organization_by_id(self) -> None: self.organization = self.create_organization(slug="test") self.organization.status = OrganizationStatus.PENDING_DELETION with assume_test_silo_mode_of(Organization): diff --git a/tests/sentry/services/hybrid_cloud/test_hybrid_cloud.py b/tests/sentry/services/hybrid_cloud/test_hybrid_cloud.py index 939040ef9d0d09..2ad6be506d8cdd 100644 --- a/tests/sentry/services/hybrid_cloud/test_hybrid_cloud.py +++ b/tests/sentry/services/hybrid_cloud/test_hybrid_cloud.py @@ -9,13 +9,13 @@ @control_silo_test class RpcModelTest(TestCase): - def test_schema_generation(self): + def test_schema_generation(self) -> None: for api_type in self._get_rpc_model_subclasses(): # We're mostly interested in whether an error occurs schema = api_type.schema_json() assert schema - def _get_rpc_model_subclasses(self): + def _get_rpc_model_subclasses(self) -> set[type[RpcModel]]: subclasses = set() stack = deque([RpcModel]) while stack: @@ -27,7 +27,7 @@ def _get_rpc_model_subclasses(self): subclasses.remove(RpcModel) return subclasses - def test_rpc_model_equals_method(self): + def test_rpc_model_equals_method(self) -> None: orm_user = self.create_user() Authenticator.objects.create(user=orm_user, type=1) diff --git a/tests/sentry/services/hybrid_cloud/test_rpcmetrics.py b/tests/sentry/services/hybrid_cloud/test_rpcmetrics.py index 163f3463b1bca1..25e022b5a8d819 100644 --- a/tests/sentry/services/hybrid_cloud/test_rpcmetrics.py +++ b/tests/sentry/services/hybrid_cloud/test_rpcmetrics.py @@ -12,14 +12,14 @@ def setUp(self) -> None: super().setUp() assert len(RpcMetricTracker.get_local().spans) == 0 - def test_single_thread(self): + def test_single_thread(self) -> None: with RpcMetricSpan() as span: for n in range(3): with RpcMetricRecord.measure(f"service{n}", f"method{n}"): pass assert len(span.records) == 3 - def test_multithreaded(self): + def test_multithreaded(self) -> None: record_queue: Queue[RpcMetricRecord] = Queue() def make_thread(n: int) -> Thread: diff --git a/tests/sentry/services/hybrid_cloud/user/test_impl.py b/tests/sentry/services/hybrid_cloud/user/test_impl.py index c3adfc3078bc5d..b8c431dc810f2a 100644 --- a/tests/sentry/services/hybrid_cloud/user/test_impl.py +++ b/tests/sentry/services/hybrid_cloud/user/test_impl.py @@ -13,7 +13,7 @@ class DatabaseBackedUserService(TestCase): def setUp(self) -> None: super().setUp() - def test_create_new_user(self): + def test_create_new_user(self) -> None: old_user_count = User.objects.all().count() rpc_user, created = user_service.get_or_create_user_by_email(email="test@email.com") user = User.objects.get(id=rpc_user.id) @@ -22,11 +22,11 @@ def test_create_new_user(self): assert user.flags.newsletter_consent_prompt assert created - def test_get_no_existing(self): + def test_get_no_existing(self) -> None: rpc_user = user_service.get_user_by_email(email="test@email.com") assert rpc_user is None - def test_get_or_create_user(self): + def test_get_or_create_user(self) -> None: user1 = self.create_user(email="test@email.com", username="1") user2 = self.create_user(email="test@email.com", username="2") user, created = user_service.get_or_create_user_by_email(email="test@email.com") @@ -34,7 +34,7 @@ def test_get_or_create_user(self): assert user2.id != user.id assert created is False - def test_get_active_user(self): + def test_get_active_user(self) -> None: inactive_user = self.create_user( email="test@email.com", username="inactive", is_active=False ) @@ -44,13 +44,13 @@ def test_get_active_user(self): assert inactive_user.id != user.id assert created is False - def test_get_user_ci(self): + def test_get_user_ci(self) -> None: user = self.create_user(email="tESt@email.com") fetched_user, created = user_service.get_or_create_user_by_email(email="TesT@email.com") assert user.id == fetched_user.id assert created is False - def test_get_user_with_ident(self): + def test_get_user_with_ident(self) -> None: user1 = self.create_user(email="test@email.com", username="1") user2 = self.create_user(email="test@email.com", username="2") org = self.create_organization(slug="test") @@ -67,7 +67,7 @@ def test_get_user_with_ident(self): assert user1.id != fetched_user.id assert created is False - def test_verify_user_emails(self): + def test_verify_user_emails(self) -> None: user1 = self.create_user(email="test@email.com") user2 = self.create_user(email="test2@email.com") verified_emails = user_service.verify_user_emails( @@ -82,7 +82,7 @@ def test_verify_user_emails(self): assert verified_emails[user1.id].exists assert not verified_emails[user2.id].exists - def test_verify_user_emails_only_verified(self): + def test_verify_user_emails_only_verified(self) -> None: user1 = self.create_user(email="test@email.com") user2 = self.create_user(email="test2@email.com") UserEmail.objects.filter(user=user2, email="test2@email.com").update(is_verified=False) From 81cffe85f6c43dfcc275e522fa7f248410c8412e Mon Sep 17 00:00:00 2001 From: Abdkhan14 <60121741+Abdkhan14@users.noreply.github.com> Date: Mon, 13 May 2024 17:31:34 -0400 Subject: [PATCH 24/31] fix(new-trace): Missing instrumentation span durations not visible. (#70716) Screenshot 2024-05-12 at 8 54 37 PM Co-authored-by: Abdullah Khan --- .../views/performance/newTraceDetails/trace.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.tsx b/static/app/views/performance/newTraceDetails/trace.tsx index 8b5db326c4e28c..1117268b3b6354 100644 --- a/static/app/views/performance/newTraceDetails/trace.tsx +++ b/static/app/views/performance/newTraceDetails/trace.tsx @@ -1325,16 +1325,18 @@ function MissingInstrumentationTraceBar(props: MissingInstrumentationTraceBarPro }, [props.manager, props.node_space, props.virtualized_index, duration] ); + return ( -
+ +
+
+
+
+
{duration}
- -
-
-
-
+ ); } From 4a732daa47154d4afaa0d80b95c56dc7ff136336 Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Mon, 13 May 2024 17:39:12 -0400 Subject: [PATCH 25/31] feat(insights): add analytics to database sample panel (#70769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds analytics to the span sample panel to track: - How often the panels are opened - How often spans are clicked - How often “Try different samples” is clicked - How often filters are interacted with This PR adds analytics to database only as a first step. I'll be adding analytics to the remaining modules in the next PR. --- .../analytics/performanceAnalyticsEvents.tsx | 25 +++++++++++++++++++ .../resources/resourceSummaryPage/index.tsx | 3 ++- .../database/databaseSpanSummaryPage.tsx | 3 ++- .../database/queryTransactionsTable.tsx | 13 +++++++++- .../samples/samplesContainer.tsx | 3 ++- .../samplesTable/spanSamplesTable.tsx | 11 +++++++- static/app/views/starfish/types.tsx | 5 ++++ .../spanSummaryPage/sampleList/index.tsx | 5 +++- .../sampleTable/sampleTable.spec.tsx | 7 +++++- .../sampleList/sampleTable/sampleTable.tsx | 17 +++++++++++-- .../views/spans/selectors/actionSelector.tsx | 5 ++++ .../views/spans/selectors/domainSelector.tsx | 5 ++++ .../starfish/views/spans/spanTimeCharts.tsx | 5 ++++ 13 files changed, 98 insertions(+), 9 deletions(-) diff --git a/static/app/utils/analytics/performanceAnalyticsEvents.tsx b/static/app/utils/analytics/performanceAnalyticsEvents.tsx index d23fd15758651d..cfe932f0d890ae 100644 --- a/static/app/utils/analytics/performanceAnalyticsEvents.tsx +++ b/static/app/utils/analytics/performanceAnalyticsEvents.tsx @@ -1,3 +1,4 @@ +import type {FieldValue} from 'sentry/components/forms/model'; import type {Organization, PlatformKey} from 'sentry/types'; type SampleTransactionParam = { @@ -127,6 +128,24 @@ export type PerformanceEventParameters = { 'performance_views.relative_breakdown.selection': { action: string; }; + 'performance_views.sample_spans.filter_updated': { + filter: string; + new_state: FieldValue; + organization: Organization; + source: string; + }; + 'performance_views.sample_spans.opened': { + organization: Organization; + source: string; + }; + 'performance_views.sample_spans.span_clicked': { + organization: Organization; + source: string; + }; + 'performance_views.sample_spans.try_different_samples_clicked': { + organization: Organization; + source: string; + }; 'performance_views.span_summary.change_chart': { change_to_display: string; }; @@ -253,6 +272,12 @@ export const performanceEventMap: Record = { 'performance_views.landingv3.table_pagination': 'Performance Views: Landing Page Transactions Table Page Changed', 'performance_views.overview.change_chart': 'Performance Views: Change Overview Chart', + 'performance_views.sample_spans.opened': 'Performance Views: Sample spans panel opened', + 'performance_views.sample_spans.span_clicked': 'Performance Views: Sample span clicked', + 'performance_views.sample_spans.try_different_samples_clicked': + 'Performance Views: Try Different Samples clicked', + 'performance_views.sample_spans.filter_updated': + 'Performance Views: Sample spans panel filter updated', 'performance_views.span_summary.change_chart': 'Performance Views: Span Summary displayed chart changed', 'performance_views.spans.change_op': 'Performance Views: Change span operation name', diff --git a/static/app/views/performance/browser/resources/resourceSummaryPage/index.tsx b/static/app/views/performance/browser/resources/resourceSummaryPage/index.tsx index 8503aca2c58bd6..6d2cd9f62990e1 100644 --- a/static/app/views/performance/browser/resources/resourceSummaryPage/index.tsx +++ b/static/app/views/performance/browser/resources/resourceSummaryPage/index.tsx @@ -26,7 +26,7 @@ import {ResourceSpanOps} from 'sentry/views/performance/browser/resources/shared import {useResourceModuleFilters} from 'sentry/views/performance/browser/resources/utils/useResourceFilters'; import {ModulePageProviders} from 'sentry/views/performance/modulePageProviders'; import {useSpanMetrics} from 'sentry/views/starfish/queries/useDiscover'; -import {SpanMetricsField} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {SampleList} from 'sentry/views/starfish/views/spanSummaryPage/sampleList'; const { @@ -149,6 +149,7 @@ function ResourceSummary() { diff --git a/static/app/views/performance/database/databaseSpanSummaryPage.tsx b/static/app/views/performance/database/databaseSpanSummaryPage.tsx index 2dcd5efd52f9cc..e66cdd7e9128f4 100644 --- a/static/app/views/performance/database/databaseSpanSummaryPage.tsx +++ b/static/app/views/performance/database/databaseSpanSummaryPage.tsx @@ -31,7 +31,7 @@ import {getTimeSpentExplanation} from 'sentry/views/starfish/components/tableCel import {useSpanMetrics} from 'sentry/views/starfish/queries/useDiscover'; import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useDiscoverSeries'; import type {SpanMetricsQueryFilters} from 'sentry/views/starfish/types'; -import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types'; +import {ModuleName, SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types'; import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters'; import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types'; import {SampleList} from 'sentry/views/starfish/views/spanSummaryPage/sampleList'; @@ -266,6 +266,7 @@ export function DatabaseSpanSummaryPage({params}: Props) { diff --git a/static/app/views/performance/database/queryTransactionsTable.tsx b/static/app/views/performance/database/queryTransactionsTable.tsx index d4686b1e843135..3f224c3c58403a 100644 --- a/static/app/views/performance/database/queryTransactionsTable.tsx +++ b/static/app/views/performance/database/queryTransactionsTable.tsx @@ -10,6 +10,7 @@ import type {CursorHandler} from 'sentry/components/pagination'; import Pagination from 'sentry/components/pagination'; import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types'; +import {trackAnalytics} from 'sentry/utils/analytics'; import type {EventsMetaType} from 'sentry/utils/discover/eventView'; import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import type {Sort} from 'sentry/utils/discover/fields'; @@ -161,7 +162,17 @@ function renderBodyCell( return ( - {label} + + trackAnalytics('performance_views.sample_spans.opened', { + organization, + source: 'database', + }) + } + to={`${pathname}?${qs.stringify(query)}`} + > + {label} + ); } diff --git a/static/app/views/performance/mobile/screenload/screenLoadSpans/samples/samplesContainer.tsx b/static/app/views/performance/mobile/screenload/screenLoadSpans/samples/samplesContainer.tsx index f44c00fdaf3a6e..8893bc46fd04e6 100644 --- a/static/app/views/performance/mobile/screenload/screenLoadSpans/samples/samplesContainer.tsx +++ b/static/app/views/performance/mobile/screenload/screenLoadSpans/samples/samplesContainer.tsx @@ -24,7 +24,7 @@ import { import {isCrossPlatform} from 'sentry/views/performance/mobile/screenload/screens/utils'; import {useSpanMetrics} from 'sentry/views/starfish/queries/useDiscover'; import type {SpanMetricsQueryFilters} from 'sentry/views/starfish/types'; -import {SpanMetricsField} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import {formatVersionAndCenterTruncate} from 'sentry/views/starfish/utils/centerTruncate'; import {DataTitles} from 'sentry/views/starfish/views/spans/types'; import DurationChart from 'sentry/views/starfish/views/spanSummaryPage/sampleList/durationChart'; @@ -182,6 +182,7 @@ export function ScreenLoadSampleContainer({ onMouseOverSample={sample => setHighlightedSpanId(sample.span_id)} groupId={groupId} transactionName={transactionName} + moduleName={ModuleName.SCREEN} release={release} columnOrder={[ { diff --git a/static/app/views/starfish/components/samplesTable/spanSamplesTable.tsx b/static/app/views/starfish/components/samplesTable/spanSamplesTable.tsx index b99db8206adb97..88496ae9e796c8 100644 --- a/static/app/views/starfish/components/samplesTable/spanSamplesTable.tsx +++ b/static/app/views/starfish/components/samplesTable/spanSamplesTable.tsx @@ -7,6 +7,7 @@ import Link from 'sentry/components/links/link'; import {Tooltip} from 'sentry/components/tooltip'; import {IconProfiling} from 'sentry/icons/iconProfiling'; import {t} from 'sentry/locale'; +import {trackAnalytics} from 'sentry/utils/analytics'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; @@ -19,7 +20,7 @@ import { TextAlignRight, } from 'sentry/views/starfish/components/textAlign'; import type {SpanSample} from 'sentry/views/starfish/queries/useSpanSamples'; -import {SpanMetricsField} from 'sentry/views/starfish/types'; +import {type ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; const {HTTP_RESPONSE_CONTENT_LENGTH} = SpanMetricsField; @@ -67,6 +68,7 @@ type Props = { avg: number; data: SpanTableRow[]; isLoading: boolean; + moduleName: ModuleName; columnOrder?: SamplesTableColumnHeader[]; highlightedSpanId?: string; onMouseLeaveSample?: () => void; @@ -77,6 +79,7 @@ export function SpanSamplesTable({ isLoading, data, avg, + moduleName, highlightedSpanId, onMouseLeaveSample, onMouseOverSample, @@ -123,6 +126,12 @@ export function SpanSamplesTable({ if (column.key === 'span_id') { return ( + trackAnalytics('performance_views.sample_spans.span_clicked', { + organization, + source: moduleName, + }) + } to={generateLinkToEventInTraceView({ eventId: row['transaction.id'], timestamp: row.timestamp, diff --git a/static/app/views/starfish/types.tsx b/static/app/views/starfish/types.tsx index 810a49e9f43ff6..8b465aeb5f5d6c 100644 --- a/static/app/views/starfish/types.tsx +++ b/static/app/views/starfish/types.tsx @@ -13,6 +13,11 @@ export enum StarfishType { export enum ModuleName { HTTP = 'http', DB = 'db', + CACHE = 'cache', + VITAL = 'vital', + QUEUE = 'queue', + SCREEN = 'screen', + STARTUP = 'startup', RESOURCE = 'resource', ALL = '', OTHER = 'other', diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx index 36c6bc81c64203..12013580abbdfe 100644 --- a/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/index.tsx @@ -18,7 +18,7 @@ import useRouter from 'sentry/utils/useRouter'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import DetailPanel from 'sentry/views/starfish/components/detailPanel'; import {DEFAULT_COLUMN_ORDER} from 'sentry/views/starfish/components/samplesTable/spanSamplesTable'; -import {SpanMetricsField} from 'sentry/views/starfish/types'; +import {type ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import DurationChart from 'sentry/views/starfish/views/spanSummaryPage/sampleList/durationChart'; import SampleInfo from 'sentry/views/starfish/views/spanSummaryPage/sampleList/sampleInfo'; import SampleTable from 'sentry/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable'; @@ -27,6 +27,7 @@ const {HTTP_RESPONSE_CONTENT_LENGTH} = SpanMetricsField; type Props = { groupId: string; + moduleName: ModuleName; transactionName: string; additionalFields?: string[]; onClose?: () => void; @@ -37,6 +38,7 @@ type Props = { export function SampleList({ groupId, + moduleName, transactionName, transactionMethod, spanDescription, @@ -171,6 +173,7 @@ export function SampleList({ onMouseLeaveSample={() => setHighlightedSpanId(undefined)} onMouseOverSample={sample => setHighlightedSpanId(sample.span_id)} groupId={groupId} + moduleName={moduleName} transactionName={transactionName} query={extraQuery} columnOrder={columnOrder} diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx index 1350af812722aa..fd4130905bb1dd 100644 --- a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.spec.tsx @@ -7,7 +7,7 @@ import { import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; import {t} from 'sentry/locale'; import type {PageFilters} from 'sentry/types/core'; -import {SpanMetricsField} from 'sentry/views/starfish/types'; +import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types'; import SampleTable from './sampleTable'; @@ -43,6 +43,7 @@ describe('SampleTable', function () { const container = render( @@ -55,6 +56,7 @@ describe('SampleTable', function () { const container = render( @@ -68,6 +70,7 @@ describe('SampleTable', function () { const container = render( @@ -89,6 +92,7 @@ describe('SampleTable', function () { const container = render( diff --git a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx index 3a185fdaa0528c..4691b9121fdf89 100644 --- a/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx +++ b/static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx @@ -16,7 +16,7 @@ import {useSpanMetrics} from 'sentry/views/starfish/queries/useDiscover'; import type {SpanSample} from 'sentry/views/starfish/queries/useSpanSamples'; import {useSpanSamples} from 'sentry/views/starfish/queries/useSpanSamples'; import {useTransactions} from 'sentry/views/starfish/queries/useTransactions'; -import type {SpanMetricsQueryFilters} from 'sentry/views/starfish/types'; +import type {ModuleName, SpanMetricsQueryFilters} from 'sentry/views/starfish/types'; import {SpanMetricsField} from 'sentry/views/starfish/types'; const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsField; @@ -27,6 +27,7 @@ const SpanSamplesTableContainer = styled('div')` type Props = { groupId: string; + moduleName: ModuleName; transactionName: string; additionalFields?: string[]; additionalFilters?: Record; @@ -41,6 +42,7 @@ type Props = { function SampleTable({ groupId, + moduleName, transactionName, highlightedSpanId, onMouseLeaveSample, @@ -152,6 +154,7 @@ function SampleTable({ hasData={spans.length > 0} > - + ); } diff --git a/static/app/views/starfish/views/spans/selectors/actionSelector.tsx b/static/app/views/starfish/views/spans/selectors/actionSelector.tsx index 24413915197299..b9518506f32a4d 100644 --- a/static/app/views/starfish/views/spans/selectors/actionSelector.tsx +++ b/static/app/views/starfish/views/spans/selectors/actionSelector.tsx @@ -102,6 +102,11 @@ const HTTP_ACTION_OPTIONS = [ const LABEL_FOR_MODULE_NAME: {[key in ModuleName]: ReactNode} = { http: t('HTTP Method'), db: t('SQL Command'), + cache: t('Action'), + vital: t('Action'), + queue: t('Action'), + screen: t('Action'), + startup: t('Action'), resource: t('Resource'), other: t('Action'), '': t('Action'), diff --git a/static/app/views/starfish/views/spans/selectors/domainSelector.tsx b/static/app/views/starfish/views/spans/selectors/domainSelector.tsx index 1cf6f4d8b03e38..4d3822de8d08bd 100644 --- a/static/app/views/starfish/views/spans/selectors/domainSelector.tsx +++ b/static/app/views/starfish/views/spans/selectors/domainSelector.tsx @@ -170,6 +170,11 @@ const LIMIT = 100; const LABEL_FOR_MODULE_NAME: {[key in ModuleName]: ReactNode} = { http: t('Host'), db: t('Table'), + cache: t('Domain'), + vital: t('Domain'), + queue: t('Domain'), + screen: t('Domain'), + startup: t('Domain'), resource: t('Resource'), other: t('Domain'), '': t('Domain'), diff --git a/static/app/views/starfish/views/spans/spanTimeCharts.tsx b/static/app/views/starfish/views/spans/spanTimeCharts.tsx index bd886e16dc70ea..957ecfb2b48a33 100644 --- a/static/app/views/starfish/views/spans/spanTimeCharts.tsx +++ b/static/app/views/starfish/views/spans/spanTimeCharts.tsx @@ -87,6 +87,11 @@ export function SpanTimeCharts({ {title: getDurationChartTitle(moduleName), Comp: DurationChart}, ], [ModuleName.DB]: [], + [ModuleName.CACHE]: [], + [ModuleName.VITAL]: [], + [ModuleName.QUEUE]: [], + [ModuleName.SCREEN]: [], + [ModuleName.STARTUP]: [], [ModuleName.RESOURCE]: features.includes( 'starfish-browser-resource-module-bundle-analysis' ) From 12e752c7552bf889b4aef9bf372a70cd24adc505 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Mon, 13 May 2024 14:59:11 -0700 Subject: [PATCH 26/31] test(ui): Use built-in test providers (#70795) --- .../eventReplay/replayClipPreview.spec.tsx | 27 +--- .../featureFeedback/feedbackModal.spec.tsx | 125 +++++++----------- static/app/views/performance/content.spec.tsx | 45 ++----- .../transactionReplays/index.spec.tsx | 34 +---- .../transactionVitals/index.spec.tsx | 63 ++++----- .../thresholdGroupRows.spec.tsx | 37 ++---- 6 files changed, 109 insertions(+), 222 deletions(-) diff --git a/static/app/components/events/eventReplay/replayClipPreview.spec.tsx b/static/app/components/events/eventReplay/replayClipPreview.spec.tsx index c75197b5595a53..02d7ce494853f0 100644 --- a/static/app/components/events/eventReplay/replayClipPreview.spec.tsx +++ b/static/app/components/events/eventReplay/replayClipPreview.spec.tsx @@ -1,5 +1,4 @@ import {duration} from 'moment'; -import {OrganizationFixture} from 'sentry-fixture/organization'; import {ProjectFixture} from 'sentry-fixture/project'; import {RRWebInitFrameEventsFixture} from 'sentry-fixture/replay/rrweb'; import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; @@ -11,8 +10,6 @@ import type {DetailedOrganization} from 'sentry/types/organization'; import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; import ReplayReader from 'sentry/utils/replays/replayReader'; import type RequestError from 'sentry/utils/requestError/requestError'; -import {OrganizationContext} from 'sentry/views/organizationContext'; -import {RouteContext} from 'sentry/views/routeContext'; import ReplayClipPreview from './replayClipPreview'; @@ -68,7 +65,8 @@ const render = ( children: React.ReactElement, orgParams: Partial = {} ) => { - const {router, routerContext} = initializeOrg({ + const {routerContext, organization} = initializeOrg({ + organization: {slug: mockOrgSlug, ...orgParams}, router: { routes: [ {path: '/'}, @@ -82,23 +80,10 @@ const render = ( }, }); - return baseRender( - - - {children} - - , - {context: routerContext} - ); + return baseRender(children, { + context: routerContext, + organization, + }); }; const mockIsFullscreen = jest.fn(); diff --git a/static/app/components/featureFeedback/feedbackModal.spec.tsx b/static/app/components/featureFeedback/feedbackModal.spec.tsx index 61e8ea0ea490ae..4a8fc43769426a 100644 --- a/static/app/components/featureFeedback/feedbackModal.spec.tsx +++ b/static/app/components/featureFeedback/feedbackModal.spec.tsx @@ -1,7 +1,6 @@ import {Fragment} from 'react'; import * as Sentry from '@sentry/react'; -import {initializeOrg} from 'sentry-test/initializeOrg'; import { act, renderGlobalModal, @@ -14,24 +13,6 @@ import * as indicators from 'sentry/actionCreators/indicator'; import {openModal} from 'sentry/actionCreators/modal'; import {FeedbackModal} from 'sentry/components/featureFeedback/feedbackModal'; import TextField from 'sentry/components/forms/fields/textField'; -import {RouteContext} from 'sentry/views/routeContext'; - -function ComponentProviders({children}: {children: React.ReactNode}) { - const {router} = initializeOrg(); - - return ( - - {children} - - ); -} describe('FeatureFeedback', function () { describe('default', function () { @@ -47,11 +28,7 @@ describe('FeatureFeedback', function () { renderGlobalModal(); act(() => - openModal(modalProps => ( - - - - )) + openModal(modalProps => ) ); // Form fields @@ -105,13 +82,11 @@ describe('FeatureFeedback', function () { act(() => openModal(modalProps => ( - - - + )) ); @@ -130,13 +105,11 @@ describe('FeatureFeedback', function () { act(() => openModal(modalProps => ( - - Test Secondary Action Link} - /> - + Test Secondary Action Link} + /> )) ); @@ -161,53 +134,51 @@ describe('FeatureFeedback', function () { act(() => openModal(modalProps => ( - - - {({Header, Body, Footer, state, onFieldChange}) => { - if (state.step === 0) { - return ( - -
First Step
- - onFieldChange('name', value)} - /> - -