metricsCardinality.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import {ComponentProps, Fragment, ReactNode, useEffect} from 'react';
  2. import {Location} from 'history';
  3. import {Organization} from 'sentry/types';
  4. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  5. import {parsePeriodToHours} from 'sentry/utils/dates';
  6. import EventView from 'sentry/utils/discover/eventView';
  7. import {canUseMetricsData} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  8. import MetricsCompatibilityQuery, {
  9. MetricsCompatibilityData,
  10. } from 'sentry/utils/performance/metricsEnhanced/metricsCompatibilityQuery';
  11. import MetricsCompatibilitySumsQuery, {
  12. MetricsCompatibilitySumData,
  13. } from 'sentry/utils/performance/metricsEnhanced/metricsCompatibilityQuerySums';
  14. import {createDefinedContext} from './utils';
  15. const UNPARAM_THRESHOLD = 0.01;
  16. const NULL_THRESHOLD = 0.01;
  17. export interface MetricDataSwitcherOutcome {
  18. forceTransactionsOnly: boolean;
  19. compatibleProjects?: number[];
  20. shouldNotifyUnnamedTransactions?: boolean;
  21. shouldWarnIncompatibleSDK?: boolean;
  22. }
  23. export interface MetricsCardinalityContext {
  24. isLoading: boolean;
  25. outcome?: MetricDataSwitcherOutcome;
  26. }
  27. type MergedMetricsData = MetricsCompatibilityData & MetricsCompatibilitySumData;
  28. const [_Provider, _useContext, _Context] =
  29. createDefinedContext<MetricsCardinalityContext>({
  30. name: 'MetricsCardinalityContext',
  31. strict: false,
  32. });
  33. /**
  34. * This provider determines whether the metrics data is storing performance information correctly before we
  35. * make dozens of requests on pages such as performance landing and dashboards.
  36. */
  37. export const MetricsCardinalityProvider = (props: {
  38. children: ReactNode;
  39. location: Location;
  40. organization: Organization;
  41. sendOutcomeAnalytics?: boolean;
  42. }) => {
  43. const isUsingMetrics = canUseMetricsData(props.organization);
  44. if (!isUsingMetrics) {
  45. return (
  46. <_Provider
  47. value={{
  48. isLoading: false,
  49. outcome: {
  50. forceTransactionsOnly: true,
  51. },
  52. }}
  53. >
  54. {props.children}
  55. </_Provider>
  56. );
  57. }
  58. const baseDiscoverProps = {
  59. location: props.location,
  60. orgSlug: props.organization.slug,
  61. cursor: '0:0:0',
  62. };
  63. const eventView = EventView.fromLocation(props.location);
  64. eventView.fields = [{field: 'tpm()'}];
  65. const _eventView = adjustEventViewTime(eventView);
  66. return (
  67. <Fragment>
  68. <MetricsCompatibilityQuery eventView={_eventView} {...baseDiscoverProps}>
  69. {compatabilityResult => (
  70. <MetricsCompatibilitySumsQuery eventView={_eventView} {...baseDiscoverProps}>
  71. {sumsResult => {
  72. const isLoading = compatabilityResult.isLoading || sumsResult.isLoading;
  73. const outcome =
  74. compatabilityResult.isLoading || sumsResult.isLoading
  75. ? undefined
  76. : getMetricsOutcome(
  77. compatabilityResult.tableData && sumsResult.tableData
  78. ? {
  79. ...compatabilityResult.tableData,
  80. ...sumsResult.tableData,
  81. }
  82. : null,
  83. !!compatabilityResult.error && !!sumsResult.error,
  84. props.organization
  85. );
  86. return (
  87. <Provider
  88. sendOutcomeAnalytics={props.sendOutcomeAnalytics}
  89. organization={props.organization}
  90. value={{
  91. isLoading,
  92. outcome,
  93. }}
  94. >
  95. {props.children}
  96. </Provider>
  97. );
  98. }}
  99. </MetricsCompatibilitySumsQuery>
  100. )}
  101. </MetricsCompatibilityQuery>
  102. </Fragment>
  103. );
  104. };
  105. const Provider = (
  106. props: ComponentProps<typeof _Provider> & {
  107. organization: Organization;
  108. sendOutcomeAnalytics?: boolean;
  109. }
  110. ) => {
  111. const fallbackFromNull = props.value.outcome?.shouldWarnIncompatibleSDK ?? false;
  112. const fallbackFromUnparam =
  113. props.value.outcome?.shouldNotifyUnnamedTransactions ?? false;
  114. const isOnMetrics = !props.value.outcome?.forceTransactionsOnly;
  115. useEffect(() => {
  116. if (!props.value.isLoading && props.sendOutcomeAnalytics) {
  117. trackAdvancedAnalyticsEvent('performance_views.mep.metrics_outcome', {
  118. organization: props.organization,
  119. is_on_metrics: isOnMetrics,
  120. fallback_from_null: fallbackFromNull,
  121. fallback_from_unparam: fallbackFromUnparam,
  122. });
  123. }
  124. }, [
  125. props.organization,
  126. props.value.isLoading,
  127. isOnMetrics,
  128. fallbackFromUnparam,
  129. fallbackFromNull,
  130. props.sendOutcomeAnalytics,
  131. ]);
  132. return <_Provider {...props}>{props.children}</_Provider>;
  133. };
  134. export const MetricsCardinalityConsumer = _Context.Consumer;
  135. export const useMetricsCardinalityContext = _useContext;
  136. /**
  137. * Logic for picking sides of metrics vs. transactions along with the associated warnings.
  138. */
  139. function getMetricsOutcome(
  140. dataCounts: MergedMetricsData | null,
  141. hasOtherFallbackCondition: boolean,
  142. organization: Organization
  143. ) {
  144. const fallbackOutcome: MetricDataSwitcherOutcome = {
  145. forceTransactionsOnly: true,
  146. };
  147. const successOutcome: MetricDataSwitcherOutcome = {
  148. forceTransactionsOnly: false,
  149. };
  150. const isOnFallbackThresolds = organization.features.includes(
  151. 'performance-mep-bannerless-ui'
  152. );
  153. if (!dataCounts) {
  154. return fallbackOutcome;
  155. }
  156. const compatibleProjects = dataCounts.compatible_projects;
  157. if (hasOtherFallbackCondition) {
  158. return fallbackOutcome;
  159. }
  160. if (!dataCounts) {
  161. return fallbackOutcome;
  162. }
  163. if (checkNoDataFallback(dataCounts)) {
  164. return fallbackOutcome;
  165. }
  166. if (checkIncompatibleData(dataCounts, isOnFallbackThresolds)) {
  167. return {
  168. shouldWarnIncompatibleSDK: true,
  169. forceTransactionsOnly: true,
  170. compatibleProjects,
  171. };
  172. }
  173. if (checkIfAllOtherData(dataCounts)) {
  174. return {
  175. shouldNotifyUnnamedTransactions: true,
  176. forceTransactionsOnly: true,
  177. compatibleProjects,
  178. };
  179. }
  180. if (checkIfPartialOtherData(dataCounts, isOnFallbackThresolds)) {
  181. return {
  182. shouldNotifyUnnamedTransactions: true,
  183. compatibleProjects,
  184. forceTransactionsOnly: false,
  185. };
  186. }
  187. return successOutcome;
  188. }
  189. /**
  190. * Fallback if no metrics found.
  191. */
  192. function checkNoDataFallback(dataCounts: MergedMetricsData) {
  193. const counts = normalizeCounts(dataCounts);
  194. return !counts.metricsCount;
  195. }
  196. /**
  197. * Fallback and warn if incompatible data found (old specific SDKs).
  198. */
  199. function checkIncompatibleData(
  200. dataCounts: MergedMetricsData,
  201. isOnFallbackThresolds: boolean
  202. ) {
  203. const counts = normalizeCounts(dataCounts);
  204. if (isOnFallbackThresolds) {
  205. const ratio = counts.nullCount / counts.metricsCount;
  206. return ratio > NULL_THRESHOLD;
  207. }
  208. return counts.nullCount > 0;
  209. }
  210. /**
  211. * Fallback and warn about unnamed transactions (specific SDKs).
  212. */
  213. function checkIfAllOtherData(dataCounts: MergedMetricsData) {
  214. const counts = normalizeCounts(dataCounts);
  215. return counts.unparamCount >= counts.metricsCount;
  216. }
  217. /**
  218. * Show metrics but warn about unnamed transactions.
  219. */
  220. function checkIfPartialOtherData(
  221. dataCounts: MergedMetricsData,
  222. isOnFallbackThresolds: boolean
  223. ) {
  224. const counts = normalizeCounts(dataCounts);
  225. if (isOnFallbackThresolds) {
  226. const ratio = counts.unparamCount / counts.metricsCount;
  227. return ratio > UNPARAM_THRESHOLD;
  228. }
  229. return counts.unparamCount > 0;
  230. }
  231. /**
  232. * Temporary function, can be removed after API changes.
  233. */
  234. function normalizeCounts({sum}: MergedMetricsData) {
  235. try {
  236. const metricsCount = Number(sum.metrics);
  237. const unparamCount = Number(sum.metrics_unparam);
  238. const nullCount = Number(sum.metrics_null);
  239. return {
  240. metricsCount,
  241. unparamCount,
  242. nullCount,
  243. };
  244. } catch (_) {
  245. return {
  246. metricsCount: 0,
  247. unparamCount: 0,
  248. nullCount: 0,
  249. };
  250. }
  251. }
  252. /**
  253. * Performance optimization to limit the amount of rows scanned before showing the landing page.
  254. */
  255. function adjustEventViewTime(eventView: EventView) {
  256. const _eventView = eventView.clone();
  257. if (!_eventView.start && !_eventView.end) {
  258. if (!_eventView.statsPeriod) {
  259. _eventView.statsPeriod = '1h';
  260. _eventView.start = undefined;
  261. _eventView.end = undefined;
  262. } else {
  263. const periodHours = parsePeriodToHours(_eventView.statsPeriod);
  264. if (periodHours > 1) {
  265. _eventView.statsPeriod = '1h';
  266. _eventView.start = undefined;
  267. _eventView.end = undefined;
  268. }
  269. }
  270. }
  271. return _eventView;
  272. }