prompts.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import {useCallback} from 'react';
  2. import type {Client} from 'sentry/api';
  3. import type {Organization, OrganizationSummary} from 'sentry/types/organization';
  4. import {promptIsDismissed} from 'sentry/utils/promptIsDismissed';
  5. import type {ApiQueryKey, UseApiQueryOptions} from 'sentry/utils/queryClient';
  6. import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
  7. import useApi from 'sentry/utils/useApi';
  8. type PromptsUpdateParams = {
  9. /**
  10. * The prompt feature name
  11. */
  12. feature: string;
  13. organization: OrganizationSummary;
  14. status: 'snoozed' | 'dismissed';
  15. /**
  16. * The numeric project ID as a string
  17. */
  18. projectId?: string;
  19. };
  20. /**
  21. * Update the status of a prompt
  22. */
  23. export function promptsUpdate(api: Client, params: PromptsUpdateParams) {
  24. const url = `/organizations/${params.organization.slug}/prompts-activity/`;
  25. return api.requestPromise(url, {
  26. method: 'PUT',
  27. data: {
  28. organization_id: params.organization.id,
  29. project_id: params.projectId,
  30. feature: params.feature,
  31. status: params.status,
  32. },
  33. });
  34. }
  35. type PromptCheckParams = {
  36. /**
  37. * The prompt feature name
  38. */
  39. feature: string | string[];
  40. organization: OrganizationSummary;
  41. /**
  42. * The numeric project ID as a string
  43. */
  44. projectId?: string;
  45. };
  46. /**
  47. * Raw response data from the endpoint
  48. */
  49. export type PromptResponseItem = {
  50. /**
  51. * Time since dismissed
  52. */
  53. dismissed_ts?: number;
  54. /**
  55. * Time since snoozed
  56. */
  57. snoozed_ts?: number;
  58. };
  59. export type PromptResponse = {
  60. data?: PromptResponseItem;
  61. features?: {[key: string]: PromptResponseItem};
  62. };
  63. /**
  64. * Processed endpoint response data
  65. */
  66. export type PromptData = null | {
  67. /**
  68. * Time since dismissed
  69. */
  70. dismissedTime?: number;
  71. /**
  72. * Time since snoozed
  73. */
  74. snoozedTime?: number;
  75. };
  76. /**
  77. * Get the status of a prompt
  78. */
  79. export async function promptsCheck(
  80. api: Client,
  81. params: PromptCheckParams
  82. ): Promise<PromptData> {
  83. const query = {
  84. feature: params.feature,
  85. organization_id: params.organization.id,
  86. ...(params.projectId === undefined ? {} : {project_id: params.projectId}),
  87. };
  88. const url = `/organizations/${params.organization.slug}/prompts-activity/`;
  89. const response: PromptResponse = await api.requestPromise(url, {
  90. query,
  91. });
  92. if (response?.data) {
  93. return {
  94. dismissedTime: response.data.dismissed_ts,
  95. snoozedTime: response.data.snoozed_ts,
  96. };
  97. }
  98. return null;
  99. }
  100. export const makePromptsCheckQueryKey = ({
  101. feature,
  102. organization,
  103. projectId,
  104. }: PromptCheckParams): ApiQueryKey => {
  105. const url = `/organizations/${organization.slug}/prompts-activity/`;
  106. return [
  107. url,
  108. {query: {feature, organization_id: organization.id, project_id: projectId}},
  109. ];
  110. };
  111. export function usePromptsCheck(
  112. {feature, organization, projectId}: PromptCheckParams,
  113. options: Partial<UseApiQueryOptions<PromptResponse>> = {}
  114. ) {
  115. return useApiQuery<PromptResponse>(
  116. makePromptsCheckQueryKey({feature, organization, projectId}),
  117. {
  118. staleTime: 120000,
  119. retry: false,
  120. ...options,
  121. }
  122. );
  123. }
  124. export function usePrompt({
  125. feature,
  126. organization,
  127. projectId,
  128. daysToSnooze,
  129. options,
  130. }: {
  131. feature: string;
  132. organization: Organization;
  133. daysToSnooze?: number;
  134. options?: Partial<UseApiQueryOptions<PromptResponse>>;
  135. projectId?: string;
  136. }) {
  137. const api = useApi({persistInFlight: true});
  138. const prompt = usePromptsCheck({feature, organization, projectId}, options);
  139. const queryClient = useQueryClient();
  140. const isPromptDismissed =
  141. prompt.isSuccess && prompt.data.data
  142. ? promptIsDismissed(
  143. {
  144. dismissedTime: prompt.data.data.dismissed_ts,
  145. snoozedTime: prompt.data.data.snoozed_ts,
  146. },
  147. daysToSnooze
  148. )
  149. : undefined;
  150. const dismissPrompt = useCallback(() => {
  151. promptsUpdate(api, {
  152. organization,
  153. projectId,
  154. feature,
  155. status: 'dismissed',
  156. });
  157. // Update cached query data
  158. // Will set prompt to dismissed
  159. setApiQueryData<PromptResponse>(
  160. queryClient,
  161. makePromptsCheckQueryKey({
  162. organization,
  163. feature,
  164. projectId,
  165. }),
  166. () => {
  167. const dimissedTs = new Date().getTime() / 1000;
  168. return {
  169. data: {dismissed_ts: dimissedTs},
  170. features: {[feature]: {dismissed_ts: dimissedTs}},
  171. };
  172. }
  173. );
  174. }, [api, feature, organization, projectId, queryClient]);
  175. const snoozePrompt = useCallback(() => {
  176. promptsUpdate(api, {
  177. organization,
  178. projectId,
  179. feature,
  180. status: 'snoozed',
  181. });
  182. // Update cached query data
  183. // Will set prompt to snoozed
  184. setApiQueryData<PromptResponse>(
  185. queryClient,
  186. makePromptsCheckQueryKey({
  187. organization,
  188. feature,
  189. projectId,
  190. }),
  191. () => {
  192. const snoozedTs = new Date().getTime() / 1000;
  193. return {
  194. data: {snoozed_ts: snoozedTs},
  195. features: {[feature]: {snoozed_ts: snoozedTs}},
  196. };
  197. }
  198. );
  199. }, [api, feature, organization, projectId, queryClient]);
  200. return {
  201. isLoading: prompt.isLoading,
  202. isError: prompt.isError,
  203. isPromptDismissed,
  204. dismissPrompt,
  205. snoozePrompt,
  206. };
  207. }
  208. /**
  209. * Get the status of many prompts
  210. */
  211. export async function batchedPromptsCheck<T extends readonly string[]>(
  212. api: Client,
  213. features: T,
  214. params: {
  215. organization: OrganizationSummary;
  216. projectId?: string;
  217. }
  218. ): Promise<{[key in T[number]]: PromptData}> {
  219. const query = {
  220. feature: features,
  221. organization_id: params.organization.id,
  222. ...(params.projectId === undefined ? {} : {project_id: params.projectId}),
  223. };
  224. const url = `/organizations/${params.organization.slug}/prompts-activity/`;
  225. const response: PromptResponse = await api.requestPromise(url, {
  226. query,
  227. });
  228. const responseFeatures = response?.features;
  229. const result: {[key in T[number]]?: PromptData} = {};
  230. if (!responseFeatures) {
  231. return result as {[key in T[number]]: PromptData};
  232. }
  233. for (const featureName of features) {
  234. const item = responseFeatures[featureName];
  235. if (item) {
  236. result[featureName] = {
  237. dismissedTime: item.dismissed_ts,
  238. snoozedTime: item.snoozed_ts,
  239. };
  240. } else {
  241. result[featureName] = null;
  242. }
  243. }
  244. return result as {[key in T[number]]: PromptData};
  245. }