dataConsentModal.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import missionControl from 'getsentry-images/missionControl.jpg';
  4. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  5. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  6. import {updateOrganization} from 'sentry/actionCreators/organizations';
  7. import {Button, LinkButton} from 'sentry/components/button';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import {IconClose, IconFix, IconLock} from 'sentry/icons';
  10. import {IconGraphBar} from 'sentry/icons/iconGraphBar';
  11. import {t} from 'sentry/locale';
  12. import {space} from 'sentry/styles/space';
  13. import type {Organization} from 'sentry/types/organization';
  14. import {useMutation} from 'sentry/utils/queryClient';
  15. import useApi from 'sentry/utils/useApi';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
  18. export default function DataConsentModal({closeModal}: ModalRenderProps) {
  19. const organization = useOrganization();
  20. const api = useApi();
  21. const {mutate: updateOrganizationOption, isPending} = useMutation<Organization>({
  22. mutationFn: () =>
  23. api.requestPromise(`/organizations/${organization.slug}/data-consent/`, {
  24. method: 'PUT',
  25. data: {aggregatedDataConsent: true},
  26. }),
  27. onSuccess: () => {
  28. closeModal();
  29. addSuccessMessage(t('Updated data consent settings.'));
  30. updateOrganization({id: organization.id, aggregatedDataConsent: true});
  31. },
  32. onError: () => {
  33. addErrorMessage(t('Failed to update data consent settings.'));
  34. },
  35. });
  36. return (
  37. <Fragment>
  38. <ImageHeader />
  39. <DismissButton
  40. analyticsEventKey="data_consent_banner.dismissed"
  41. analyticsEventName="Data Consent Banner: Dismissed"
  42. size="zero"
  43. borderless
  44. icon={<IconClose size="xs" />}
  45. aria-label={t('Dismiss')}
  46. onClick={() => closeModal()}
  47. />
  48. <div>
  49. <Subheader>{t('Less noise, more action')}</Subheader>
  50. <Title>{t('Help Sentry be more opinionated')}</Title>
  51. <Body>
  52. {t(
  53. "We're working to improve grouping, alert relevance, issue prioritization, and more, and we need your help."
  54. )}
  55. </Body>
  56. <InfoHeader>
  57. <ConsentHeader>{t('Data Consent')}</ConsentHeader>
  58. <LearnMore
  59. href="https://docs.sentry.io/product/security/ai-ml-policy/"
  60. onClick={() =>
  61. trackGetsentryAnalytics('data_consent_modal.learn_more', {organization})
  62. }
  63. >
  64. {t('Learn More')}
  65. </LearnMore>
  66. </InfoHeader>
  67. <ConsentInfo>
  68. <ConsentRow>
  69. <StyledIconWrapper>
  70. <IconGraphBar size="lg" />
  71. </StyledIconWrapper>
  72. <ConsentLabel>
  73. <ConsentLabelHeader>{t('What data do we access?')}</ConsentLabelHeader>
  74. <ConsentLabelBody>
  75. {t(
  76. 'Sentry will access error messages, stack traces, spans, and DOM interactions.'
  77. )}
  78. </ConsentLabelBody>
  79. </ConsentLabel>
  80. </ConsentRow>
  81. <Divider />
  82. <ConsentRow>
  83. <StyledIconWrapper>
  84. <IconFix size="lg" />
  85. </StyledIconWrapper>
  86. <ConsentLabel>
  87. <ConsentLabelHeader>{t('How do we use it?')}</ConsentLabelHeader>
  88. <ConsentLabelBody>
  89. {t(
  90. 'The data will be used to train and validate models to improve our product.'
  91. )}
  92. </ConsentLabelBody>
  93. </ConsentLabel>
  94. </ConsentRow>
  95. <Divider />
  96. <ConsentRow>
  97. <StyledIconWrapper>
  98. <IconLock locked size="lg" />
  99. </StyledIconWrapper>
  100. <ConsentLabel>
  101. <ConsentLabelHeader>{t('Where does it go?')}</ConsentLabelHeader>
  102. <ConsentLabelBody>
  103. {t(
  104. "We store data within Sentry's standard infrastructure. We will not share it with other customers or AI sub-processors without additional consent."
  105. )}
  106. </ConsentLabelBody>
  107. </ConsentLabel>
  108. </ConsentRow>
  109. </ConsentInfo>
  110. </div>
  111. <Footer>
  112. <Button
  113. analyticsEventKey="data_consent_modal.maybe_later"
  114. analyticsEventName="Data Consent Modal: Maybe Later"
  115. busy={isPending}
  116. onClick={() => {
  117. closeModal();
  118. }}
  119. >
  120. {t('Maybe later')}
  121. </Button>
  122. <LinkButton
  123. analyticsEventKey="data_consent_modal.settings"
  124. analyticsEventName="Data Consent Modal: Settings"
  125. href="/settings/legal/#aggregatedDataConsent"
  126. busy={isPending}
  127. >
  128. {t('View Settings')}
  129. </LinkButton>
  130. <Button
  131. analyticsEventKey="data_consent_modal.accepted"
  132. analyticsEventName="Data Consent Modal: Accepted"
  133. onClick={() => {
  134. updateOrganizationOption();
  135. }}
  136. priority="primary"
  137. busy={isPending}
  138. >
  139. {t('I agree')}
  140. </Button>
  141. </Footer>
  142. </Fragment>
  143. );
  144. }
  145. const Title = styled('h3')`
  146. margin-bottom: ${space(1)};
  147. `;
  148. const Subheader = styled('p')`
  149. text-transform: uppercase;
  150. color: ${p => p.theme.pink300};
  151. font-size: ${p => p.theme.fontSizeMedium};
  152. font-weight: bold;
  153. margin-bottom: ${space(1)};
  154. `;
  155. const Body = styled('div')`
  156. font-size: ${p => p.theme.fontSizeLarge};
  157. margin-bottom: ${space(2)};
  158. `;
  159. const InfoHeader = styled('div')`
  160. display: flex;
  161. flex-direction: row;
  162. justify-content: space-between;
  163. `;
  164. const ConsentHeader = styled('p')`
  165. font-weight: bold;
  166. color: ${p => p.theme.gray300};
  167. text-transform: uppercase;
  168. margin-bottom: ${space(1)};
  169. `;
  170. const ConsentInfo = styled('div')`
  171. background-color: ${p => p.theme.backgroundSecondary};
  172. border-radius: ${p => p.theme.borderRadius};
  173. padding-top: ${space(1.5)};
  174. padding-bottom: ${space(1.5)};
  175. `;
  176. const ConsentRow = styled('div')`
  177. display: flex;
  178. flex-direction: row;
  179. align-items: center;
  180. gap: ${space(3)};
  181. `;
  182. const ConsentLabel = styled('div')`
  183. display: flex;
  184. flex-direction: column;
  185. `;
  186. const ConsentLabelHeader = styled('div')`
  187. font-weight: 600;
  188. font-size: ${p => p.theme.fontSizeLarge};
  189. `;
  190. const ConsentLabelBody = styled('p')`
  191. margin-bottom: 0;
  192. color: ${p => p.theme.gray300};
  193. font-size: ${p => p.theme.fontSizeMedium};
  194. `;
  195. const StyledIconWrapper = styled('span')`
  196. margin-left: ${space(3)};
  197. color: ${p => p.theme.gray300};
  198. `;
  199. const Footer = styled('div')`
  200. display: flex;
  201. flex-direction: row;
  202. justify-content: right;
  203. gap: ${space(1)};
  204. margin-top: ${space(3)};
  205. `;
  206. const LearnMore = styled(ExternalLink)`
  207. font-weight: bold;
  208. text-transform: uppercase;
  209. &:hover {
  210. text-decoration: underline;
  211. text-decoration-color: ${p => p.theme.blue200};
  212. }
  213. `;
  214. const ImageHeader = styled('div')`
  215. margin: -${space(4)} -${space(4)} 0 -${space(4)};
  216. border-radius: ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0 0;
  217. background-image: url(${missionControl});
  218. background-size: cover;
  219. background-repeat: no-repeat;
  220. overflow: hidden;
  221. background-position: center;
  222. height: 200px;
  223. clip-path: polygon(100% 0%, 0% 0%, 0% 85%, 15% 75%, 80% 95%, 90% 85%, 100% 85%);
  224. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  225. margin: -${space(4)} -${space(3)} 0 -${space(3)};
  226. }
  227. `;
  228. const Divider = styled('hr')`
  229. width: 95%;
  230. height: 1px;
  231. background: ${p => p.theme.gray100};
  232. border: none;
  233. margin-top: ${space(1.5)};
  234. margin-bottom: ${space(1.5)};
  235. `;
  236. const DismissButton = styled(Button)`
  237. position: absolute;
  238. top: ${space(1)};
  239. right: ${space(1)};
  240. color: ${p => p.theme.subText};
  241. z-index: 1;
  242. background-color: rgba(255, 255, 255, 0.8);
  243. border-radius: 50%;
  244. `;