useSuspectFlags.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import {useEffect, useMemo} from 'react';
  2. import intersection from 'lodash/intersection';
  3. import moment from 'moment-timezone';
  4. import type {Event} from 'sentry/types/event';
  5. import type {Organization} from 'sentry/types/organization';
  6. import {trackAnalytics} from 'sentry/utils/analytics';
  7. import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient';
  8. import type RequestError from 'sentry/utils/requestError/requestError';
  9. import {
  10. hydrateToFlagSeries,
  11. type RawFlag,
  12. type RawFlagData,
  13. } from 'sentry/views/issueDetails/streamline/featureFlagUtils';
  14. export default function useSuspectFlags({
  15. organization,
  16. firstSeen,
  17. rawFlagData,
  18. event,
  19. }: {
  20. event: Event | undefined;
  21. firstSeen: string;
  22. organization: Organization;
  23. rawFlagData: RawFlagData | undefined;
  24. }): UseApiQueryResult<RawFlagData, RequestError> & {suspectFlags: RawFlag[]} {
  25. const hydratedFlagData = hydrateToFlagSeries(rawFlagData);
  26. // map flag data to arrays of flag names
  27. const auditLogFlagNames = hydratedFlagData.map(f => f.name);
  28. const evaluatedFlagNames = event?.contexts?.flags?.values?.map(f => f.flag);
  29. const intersectionFlags = useMemo(
  30. () => intersection(auditLogFlagNames, evaluatedFlagNames),
  31. [auditLogFlagNames, evaluatedFlagNames]
  32. );
  33. // get all the audit log flag changes which happened prior to the first seen date
  34. const start = moment(firstSeen).subtract(1, 'year').format('YYYY-MM-DD HH:mm:ss');
  35. const apiQueryResponse = useApiQuery<RawFlagData>(
  36. [
  37. `/organizations/${organization.slug}/flags/logs/`,
  38. {
  39. query: {
  40. flag: intersectionFlags,
  41. start,
  42. end: firstSeen,
  43. statsPeriod: undefined,
  44. },
  45. },
  46. ],
  47. {
  48. staleTime: 0,
  49. // if no intersection, then there are no suspect flags
  50. enabled: Boolean(intersectionFlags.length),
  51. }
  52. );
  53. const {data, isError, isPending} = apiQueryResponse;
  54. // no flags in common between event evaluations and audit log
  55. // only track this analytic if there is at least 1 flag recorded
  56. // in either the audit log or the event context
  57. useEffect(() => {
  58. if (
  59. !intersectionFlags.length &&
  60. (hydratedFlagData.length || evaluatedFlagNames?.length) &&
  61. !isPending &&
  62. !isError
  63. ) {
  64. trackAnalytics('flags.event_and_suspect_flags_found', {
  65. numTotalFlags: hydratedFlagData.length,
  66. numEventFlags: 0,
  67. numSuspectFlags: 0,
  68. organization,
  69. });
  70. }
  71. }, [
  72. hydratedFlagData.length,
  73. intersectionFlags.length,
  74. organization,
  75. evaluatedFlagNames?.length,
  76. isPending,
  77. isError,
  78. ]);
  79. // remove duplicate flags - keeps the one closest to the firstSeen date
  80. // cap the number of suspect flags to the 3 closest to the firstSeen date
  81. const suspectFlags = useMemo(() => {
  82. return data
  83. ? data.data
  84. .toReversed()
  85. .filter(
  86. (rawFlag, idx, rawFlagArray) =>
  87. idx === rawFlagArray.findIndex(f => f.flag === rawFlag.flag)
  88. )
  89. .slice(0, 3)
  90. : [];
  91. }, [data]);
  92. // track the funnel from
  93. // all audit log flags -> event level flags -> suspect flags
  94. useEffect(() => {
  95. if (intersectionFlags.length && !isError && !isPending) {
  96. trackAnalytics('flags.event_and_suspect_flags_found', {
  97. numTotalFlags: hydratedFlagData.length,
  98. numEventFlags: intersectionFlags.length,
  99. numSuspectFlags: suspectFlags.length,
  100. organization,
  101. });
  102. }
  103. }, [
  104. hydratedFlagData.length,
  105. isError,
  106. isPending,
  107. suspectFlags.length,
  108. intersectionFlags.length,
  109. organization,
  110. ]);
  111. return {...apiQueryResponse, suspectFlags};
  112. }