useSuspectFlags.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  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. // no flags in common between event evaluations and audit log
  34. useEffect(() => {
  35. if (!intersectionFlags.length) {
  36. trackAnalytics('flags.event_and_suspect_flags_found', {
  37. numTotalFlags: hydratedFlagData.length,
  38. numEventFlags: 0,
  39. numSuspectFlags: 0,
  40. organization,
  41. });
  42. }
  43. }, [hydratedFlagData.length, intersectionFlags.length, organization]);
  44. // get all the audit log flag changes which happened prior to the first seen date
  45. const start = moment(firstSeen).subtract(1, 'year').format('YYYY-MM-DD HH:mm:ss');
  46. const apiQueryResponse = useApiQuery<RawFlagData>(
  47. [
  48. `/organizations/${organization.slug}/flags/logs/`,
  49. {
  50. query: {
  51. flag: intersectionFlags,
  52. start,
  53. end: firstSeen,
  54. statsPeriod: undefined,
  55. },
  56. },
  57. ],
  58. {
  59. staleTime: 0,
  60. // if no intersection, then there are no suspect flags
  61. enabled: Boolean(
  62. organization.features.includes('feature-flag-ui') && intersectionFlags.length
  63. ),
  64. }
  65. );
  66. const {data, isError, isPending} = apiQueryResponse;
  67. // remove duplicate flags - keeps the one closest to the firstSeen date
  68. // cap the number of suspect flags to the 3 closest to the firstSeen date
  69. const suspectFlags = useMemo(() => {
  70. return data
  71. ? data.data
  72. .toReversed()
  73. .filter(
  74. (rawFlag, idx, rawFlagArray) =>
  75. idx === rawFlagArray.findIndex(f => f.flag === rawFlag.flag)
  76. )
  77. .slice(0, 3)
  78. : [];
  79. }, [data]);
  80. // track the funnel from
  81. // all audit log flags -> event level flags -> suspect flags
  82. useEffect(() => {
  83. if (intersectionFlags.length && !isError && !isPending) {
  84. trackAnalytics('flags.event_and_suspect_flags_found', {
  85. numTotalFlags: hydratedFlagData.length,
  86. numEventFlags: intersectionFlags.length,
  87. numSuspectFlags: suspectFlags.length,
  88. organization,
  89. });
  90. }
  91. }, [
  92. hydratedFlagData,
  93. isError,
  94. isPending,
  95. suspectFlags,
  96. intersectionFlags,
  97. organization,
  98. ]);
  99. return {...apiQueryResponse, suspectFlags};
  100. }