samplingFeedback.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import {Fragment, useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import CheckboxFancy from 'sentry/components/checkboxFancy/checkboxFancy';
  4. import {FeatureFeedback} from 'sentry/components/featureFeedback';
  5. import {TextField} from 'sentry/components/forms';
  6. import Textarea from 'sentry/components/forms/controls/textarea';
  7. import Field from 'sentry/components/forms/field';
  8. import {RadioGroupRating} from 'sentry/components/radioGroupRating';
  9. import {t} from 'sentry/locale';
  10. import space from 'sentry/styles/space';
  11. import {defined} from 'sentry/utils';
  12. enum SamplingUsageReason {
  13. REDUCE_VOLUME_TO_STAY_WITHIN_QUOTA = 'reduce_volume_to_stay_within_quota',
  14. FILTER_OUT_NOISY_DATA = 'filter_out_noisy_data',
  15. OTHER = 'other',
  16. }
  17. enum SampleByOption {
  18. TRANSACTION_NAME = 'transaction_name',
  19. CUSTOM_TAGS = 'custom_tags',
  20. OTHER = 'other',
  21. }
  22. const featureNotAvailableRatingOptions = {
  23. 0: {
  24. label: t('Very Dissatisfied'),
  25. description: t("Not disappointed (It isn't really useful)"),
  26. },
  27. 1: {
  28. label: t('Dissatisfied'),
  29. },
  30. 2: {
  31. label: t('Neutral'),
  32. },
  33. 3: {
  34. label: t('Satisfied'),
  35. },
  36. 4: {
  37. description: t("Very disappointed (It's a deal breaker)"),
  38. label: t('Very Satisfied'),
  39. },
  40. };
  41. type Option = {
  42. checked: boolean;
  43. title: string;
  44. value: string | number;
  45. };
  46. type InitialData = {
  47. additionalFeedback: string | null;
  48. feelingIfFeatureNotAvailable: number | undefined;
  49. sampleByOptions: Option[];
  50. sampleByOtherOption: string | null;
  51. samplingUsageOtherReason: string | null;
  52. samplingUsageReasons: Option[];
  53. step: number;
  54. };
  55. const initialData: InitialData = {
  56. step: 0,
  57. samplingUsageReasons: [
  58. {
  59. title: t('Reduce volume to stay within my quota'),
  60. value: SamplingUsageReason.REDUCE_VOLUME_TO_STAY_WITHIN_QUOTA,
  61. checked: false,
  62. },
  63. {title: t('Filter out noisy data'), value: 1, checked: false},
  64. {
  65. title: t('Other'),
  66. value: SamplingUsageReason.OTHER,
  67. checked: false,
  68. },
  69. ],
  70. sampleByOtherOption: null,
  71. samplingUsageOtherReason: null,
  72. sampleByOptions: [
  73. {
  74. title: t('Transaction Name'),
  75. value: SampleByOption.TRANSACTION_NAME,
  76. checked: false,
  77. },
  78. {title: t('Custom Tags'), value: SampleByOption.CUSTOM_TAGS, checked: false},
  79. {
  80. title: t('Other'),
  81. value: SampleByOption.OTHER,
  82. checked: false,
  83. },
  84. ],
  85. additionalFeedback: null,
  86. feelingIfFeatureNotAvailable: undefined,
  87. };
  88. function MultipleCheckboxField({
  89. options,
  90. onChange,
  91. otherTextField,
  92. }: {
  93. onChange: (options: Option[]) => void;
  94. options: Option[];
  95. otherTextField: React.ReactNode;
  96. }) {
  97. const handleClick = useCallback(
  98. (newOption: Option) => {
  99. const newOptions = options.map(option => {
  100. if (option.value === newOption.value) {
  101. return {
  102. ...option,
  103. checked: !option.checked,
  104. };
  105. }
  106. return option;
  107. });
  108. onChange(newOptions);
  109. },
  110. [onChange, options]
  111. );
  112. return (
  113. <Fragment>
  114. {options.map(option => {
  115. if (option.value === 'other') {
  116. return (
  117. <CheckboxOtherOptionWrapper
  118. key={option.value}
  119. onClick={() => handleClick(option)}
  120. >
  121. <CheckboxFancy isChecked={option.checked} />
  122. {option.title}
  123. {otherTextField}
  124. </CheckboxOtherOptionWrapper>
  125. );
  126. }
  127. return (
  128. <CheckboxOption key={option.value} onClick={() => handleClick(option)}>
  129. <CheckboxFancy isChecked={option.checked} />
  130. {option.title}
  131. </CheckboxOption>
  132. );
  133. })}
  134. </Fragment>
  135. );
  136. }
  137. export function SamplingFeedback() {
  138. return (
  139. <FeatureFeedback
  140. featureName="dynamic-sampling"
  141. initialData={initialData}
  142. buttonProps={{
  143. priority: 'primary',
  144. size: 'sm',
  145. }}
  146. >
  147. {({Header, Body, Footer, state, onFieldChange}) => {
  148. if (state.step === 0) {
  149. return (
  150. <Fragment>
  151. <Header>{t('A few questions (1/2)')}</Header>
  152. <Body showSelfHostedMessage={false}>
  153. <Field
  154. label={<Label>{t('Why do you want to use Dynamic Sampling?')}</Label>}
  155. stacked
  156. inline={false}
  157. flexibleControlStateSize
  158. >
  159. <MultipleCheckboxField
  160. options={state.samplingUsageReasons}
  161. onChange={newSamplingUsageReasons => {
  162. if (
  163. newSamplingUsageReasons.some(
  164. newSamplingUsageReason =>
  165. newSamplingUsageReason.value === SamplingUsageReason.OTHER &&
  166. newSamplingUsageReason.checked === false
  167. )
  168. ) {
  169. onFieldChange('samplingUsageOtherReason', null);
  170. }
  171. onFieldChange('samplingUsageReasons', newSamplingUsageReasons);
  172. }}
  173. otherTextField={
  174. <OtherTextField
  175. inline={false}
  176. name="samplingUsageOtherReason"
  177. flexibleControlStateSize
  178. stacked
  179. disabled={state.samplingUsageReasons.some(
  180. samplingUsageReason =>
  181. samplingUsageReason.value === SamplingUsageReason.OTHER &&
  182. samplingUsageReason.checked === false
  183. )}
  184. onClick={event => event.stopPropagation()}
  185. value={state.samplingUsageOtherReason}
  186. onChange={value =>
  187. onFieldChange('samplingUsageOtherReason', value)
  188. }
  189. placeholder={t('Please kindly let us know the reason')}
  190. />
  191. }
  192. />
  193. </Field>
  194. <Field
  195. label={<Label>{t('What else you would like to sample by?')}</Label>}
  196. stacked
  197. inline={false}
  198. flexibleControlStateSize
  199. >
  200. <MultipleCheckboxField
  201. options={state.sampleByOptions}
  202. onChange={newSampleByOptions => {
  203. if (
  204. newSampleByOptions.some(
  205. sampleByOption =>
  206. sampleByOption.value === SampleByOption.OTHER &&
  207. sampleByOption.checked === false
  208. )
  209. ) {
  210. onFieldChange('sampleByOtherOption', null);
  211. }
  212. onFieldChange('sampleByOptions', newSampleByOptions);
  213. }}
  214. otherTextField={
  215. <OtherTextField
  216. inline={false}
  217. name="sampleByOtherOption"
  218. flexibleControlStateSize
  219. stacked
  220. disabled={state.sampleByOptions.some(
  221. sampleByOption =>
  222. sampleByOption.value === SampleByOption.OTHER &&
  223. sampleByOption.checked === false
  224. )}
  225. onClick={event => event.stopPropagation()}
  226. value={state.sampleByOtherOption}
  227. onChange={value => onFieldChange('sampleByOtherOption', value)}
  228. placeholder={t('Please let us know which other attributes')}
  229. />
  230. }
  231. />
  232. </Field>
  233. </Body>
  234. <Footer onNext={() => onFieldChange('step', 1)} />
  235. </Fragment>
  236. );
  237. }
  238. const submitEventData = {
  239. contexts: {
  240. survey: {
  241. samplingUsageReasons:
  242. state.samplingUsageReasons
  243. .filter(samplingUsageReason => samplingUsageReason.checked)
  244. .map(samplingUsageReason => samplingUsageReason.title)
  245. .join(', ') || null,
  246. samplingUsageOtherReason: state.samplingUsageOtherReason,
  247. sampleByOptions:
  248. state.sampleByOptions
  249. .filter(sampleByOption => sampleByOption.checked)
  250. .map(sampleByOption => sampleByOption.title)
  251. .join(', ') || null,
  252. sampleByOtherOption: state.sampleByOtherOption,
  253. additionalFeedback: state.additionalFeedback,
  254. feelingIfFeatureNotAvailable: defined(state.feelingIfFeatureNotAvailable)
  255. ? featureNotAvailableRatingOptions[state.feelingIfFeatureNotAvailable]
  256. .label
  257. : null,
  258. },
  259. },
  260. message: state.additionalFeedback
  261. ? `Feedback: 'dynamic sampling' feature - ${state.additionalFeedback}`
  262. : `Feedback: 'dynamic sampling' feature`,
  263. };
  264. const primaryButtonDisabled = Object.keys(submitEventData.contexts.survey).every(
  265. s => {
  266. const value = submitEventData.contexts.survey[s] ?? null;
  267. if (typeof value === 'string') {
  268. return value.trim() === '';
  269. }
  270. return value === null;
  271. }
  272. );
  273. return (
  274. <Fragment>
  275. <Header>{t('A few questions (2/2)')}</Header>
  276. <Body>
  277. <RadioGroupRating
  278. label={
  279. <Label>
  280. {t('How would you feel if you could no longer use this feature?')}
  281. </Label>
  282. }
  283. inline={false}
  284. required={false}
  285. flexibleControlStateSize
  286. stacked
  287. options={featureNotAvailableRatingOptions}
  288. name="feelingIfFeatureNotAvailableRating"
  289. defaultValue={
  290. defined(state.feelingIfFeatureNotAvailable)
  291. ? String(state.feelingIfFeatureNotAvailable)
  292. : undefined
  293. }
  294. onChange={value =>
  295. onFieldChange('feelingIfFeatureNotAvailable', Number(value))
  296. }
  297. />
  298. <Field
  299. label={<Label>{t('Anything else you would like to share?')}</Label>}
  300. inline={false}
  301. required={false}
  302. flexibleControlStateSize
  303. stacked
  304. >
  305. <Textarea
  306. name="additional-feedback"
  307. value={state.additionalFeedback ?? undefined}
  308. rows={5}
  309. autosize
  310. onChange={event =>
  311. onFieldChange('additionalFeedback', event.target.value)
  312. }
  313. placeholder={t('Additional feedback')}
  314. />
  315. </Field>
  316. </Body>
  317. <Footer
  318. onBack={() => onFieldChange('step', 0)}
  319. primaryDisabledReason={
  320. primaryButtonDisabled
  321. ? t('Please answer at least one question')
  322. : undefined
  323. }
  324. submitEventData={submitEventData}
  325. />
  326. </Fragment>
  327. );
  328. }}
  329. </FeatureFeedback>
  330. );
  331. }
  332. const Label = styled('strong')`
  333. margin-bottom: ${space(1)};
  334. display: inline-block;
  335. `;
  336. const CheckboxOption = styled('div')`
  337. cursor: pointer;
  338. display: grid;
  339. grid-template-columns: max-content 1fr;
  340. gap: ${space(1)};
  341. align-items: center;
  342. :not(:last-child) {
  343. margin-bottom: ${space(1)};
  344. }
  345. `;
  346. const CheckboxOtherOptionWrapper = styled(CheckboxOption)`
  347. grid-template-columns: max-content max-content 1fr;
  348. @media (max-width: ${p => p.theme.breakpoints.small}) {
  349. grid-template-columns: max-content 1fr;
  350. }
  351. `;
  352. const OtherTextField = styled(TextField)`
  353. @media (max-width: ${p => p.theme.breakpoints.small}) {
  354. grid-column: 1/-1;
  355. }
  356. && {
  357. input {
  358. ${p => p.disabled && 'cursor: pointer;'}
  359. }
  360. }
  361. `;