Browse Source

feat(feedback): Remove old feedback widget and replace with SDK version (#58305)

This will be disabled until we add the feedback widget in getsentry.
Billy Vong 1 year ago
parent
commit
19f7f07bd3

+ 1 - 0
package.json

@@ -48,6 +48,7 @@
     "@react-stately/tabs": "^3.4.1",
     "@react-stately/tree": "^3.6.1",
     "@react-types/shared": "^3.18.1",
+    "@sentry-internal/feedback": "0.0.1-alpha.4",
     "@sentry-internal/global-search": "^0.5.7",
     "@sentry-internal/react-inspector": "6.0.1-4",
     "@sentry-internal/rrweb": "2.1.1",

File diff suppressed because it is too large
+ 0 - 87
static/app/components/feedback/widget/feedbackButton.tsx


+ 0 - 158
static/app/components/feedback/widget/feedbackForm.tsx

@@ -1,158 +0,0 @@
-import {FormEvent, useRef, useState} from 'react';
-import {css} from '@emotion/react';
-import styled from '@emotion/styled';
-import {getCurrentHub} from '@sentry/react';
-
-interface FeedbackFormProps {
-  descriptionPlaceholder: string;
-  onClose: () => void;
-  onSubmit: (data: {comment: string; email: string; name: string}) => void;
-  sendButtonText: string;
-}
-
-const retrieveStringValue = (formData: FormData, key: string) => {
-  const value = formData.get(key);
-  if (typeof value === 'string') {
-    return value.trim();
-  }
-  return '';
-};
-
-export function FeedbackForm({
-  descriptionPlaceholder,
-  sendButtonText,
-  onClose,
-  onSubmit,
-}: FeedbackFormProps) {
-  const formRef = useRef<HTMLFormElement>(null);
-  const [hasDescription, setHasDescription] = useState(false);
-
-  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
-    e.preventDefault();
-    const formData = new FormData(e.target as HTMLFormElement);
-
-    onSubmit({
-      name: retrieveStringValue(formData, 'name'),
-      email: retrieveStringValue(formData, 'email'),
-      comment: retrieveStringValue(formData, 'comment'),
-    });
-  };
-
-  const user = getCurrentHub().getScope()?.getUser();
-
-  return (
-    <Form ref={formRef} onSubmit={handleSubmit}>
-      <Input
-        type="hidden"
-        name="name"
-        aria-hidden
-        defaultValue={user?.username || user?.name}
-      />
-      <Input type="hidden" name="email" defaultValue={user?.email} hidden aria-hidden />
-      <Label htmlFor="sentry-feedback-comment">
-        <div>Description</div>
-        <TextArea
-          autoFocus
-          rows={5}
-          onChange={event => {
-            setHasDescription(!!event.target.value);
-          }}
-          onKeyDown={event => {
-            if (event.key === 'Enter' && event.ctrlKey) {
-              formRef.current?.requestSubmit();
-            }
-          }}
-          id="sentry-feedback-comment"
-          name="comment"
-          placeholder={descriptionPlaceholder}
-        />
-      </Label>
-      <ButtonGroup>
-        <SubmitButton
-          type="submit"
-          disabled={!hasDescription}
-          aria-disabled={!hasDescription}
-        >
-          {sendButtonText}
-        </SubmitButton>
-        <CancelButton type="button" onClick={onClose}>
-          Cancel
-        </CancelButton>
-      </ButtonGroup>
-    </Form>
-  );
-}
-
-const Form = styled('form')`
-  display: grid;
-  overflow: auto;
-  flex-direction: column;
-  gap: 16px;
-  padding: 0;
-`;
-
-const Label = styled('label')`
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  margin: 0px;
-`;
-
-const inputStyles = css`
-  box-sizing: border-box;
-  border: var(--sentry-feedback-border);
-  border-radius: 6px;
-  font-size: 14px;
-  font-weight: 500;
-  padding: 6px 12px;
-  &:focus {
-    border-color: rgba(108, 95, 199, 1);
-  }
-`;
-
-const Input = styled('input')`
-  ${inputStyles}
-`;
-
-const TextArea = styled('textarea')`
-  ${inputStyles}
-  resize: vertical;
-`;
-
-const ButtonGroup = styled('div')`
-  display: grid;
-  gap: 8px;
-  margin-top: 8px;
-`;
-
-const BaseButton = styled('button')`
-  border: var(--sentry-feedback-border);
-  border-radius: 6px;
-  cursor: pointer;
-  font-size: 14px;
-  font-weight: 600;
-  padding: 6px 16px;
-`;
-
-const SubmitButton = styled(BaseButton)`
-  background-color: rgba(108, 95, 199, 1);
-  border-color: rgba(108, 95, 199, 1);
-  color: #fff;
-  &:hover {
-    background-color: rgba(88, 74, 192, 1);
-  }
-
-  &[disabled] {
-    opacity: 0.6;
-    pointer-events: none;
-  }
-`;
-
-const CancelButton = styled(BaseButton)`
-  background-color: transparent;
-  color: var(--sentry-feedback-fg-color);
-  font-weight: 500;
-  &:hover {
-    background-color: var(--sentry-feedback-bg-accent-color);
-  }
-`;

+ 0 - 233
static/app/components/feedback/widget/feedbackModal.tsx

@@ -1,233 +0,0 @@
-import React, {Fragment, useEffect, useRef, useState} from 'react';
-import styled from '@emotion/styled';
-import {getCurrentHub, Replay} from '@sentry/react';
-import classNames from 'classnames';
-
-import useKeyPress from 'sentry/utils/useKeyPress';
-
-import {FeedbackForm} from './feedbackForm';
-import {FeedbackSuccessMessage} from './feedbackSuccessMessage';
-import {sendFeedbackRequest} from './sendFeedbackRequest';
-import {useFocusTrap} from './useFocusTrap';
-
-interface RenderFunctionProps {
-  /**
-   * Is the modal open/visible
-   */
-  open: boolean;
-
-  /**
-   * Shows the feedback modal
-   */
-  showModal: () => void;
-}
-type FeedbackRenderFunction = (
-  renderFunctionProps: RenderFunctionProps
-) => React.ReactNode;
-
-interface FeedbackModalProps {
-  children: FeedbackRenderFunction;
-  title: string;
-  className?: string;
-  descriptionPlaceholder?: string;
-  sendButtonText?: string;
-  type?: string;
-  widgetTheme?: 'dark' | 'light';
-}
-
-interface FeedbackFormData {
-  comment: string;
-  email: string;
-  name: string;
-}
-
-async function sendFeedback(
-  data: FeedbackFormData,
-  pageUrl: string,
-  replayId?: string,
-  type?: string
-): Promise<Response | null> {
-  const feedback = {
-    message: data.comment,
-    email: data.email,
-    name: data.name,
-    replay_id: replayId,
-    url: pageUrl,
-  };
-  const tags = type ? {feedbackType: type} : null;
-  return await sendFeedbackRequest({feedback, tags});
-}
-
-function stopPropagation(e: React.MouseEvent) {
-  e.stopPropagation();
-}
-
-/**
- * Feedback widget's modal container
- *
- * XXX: This is only temporary as we move this to SDK
- */
-export function FeedbackModal({
-  className,
-  descriptionPlaceholder = "What's the bug? What did you expect?",
-  sendButtonText = 'Send Bug Report',
-  widgetTheme = 'light',
-  title,
-  type,
-  children,
-}: FeedbackModalProps) {
-  const [open, setOpen] = useState(false);
-  const [errorMessage, setError] = useState('');
-  const dialogRef = useRef<HTMLDialogElement>(null);
-  const [showSuccessMessage, setShowSuccessMessage] = useState(false);
-  const escapePressed = useKeyPress('Escape');
-  const isDarkTheme = widgetTheme === 'dark';
-
-  const handleClose = () => {
-    setOpen(false);
-  };
-
-  const handleSubmit = (data: FeedbackFormData) => {
-    const replay = getCurrentHub()?.getClient()?.getIntegration(Replay);
-
-    // Prepare session replay
-    replay?.flush();
-    const replayId = replay?.getReplayId();
-
-    const pageUrl = document.location.href;
-
-    sendFeedback(data, pageUrl, replayId, type).then(response => {
-      if (response) {
-        setOpen(false);
-        setShowSuccessMessage(true);
-        setError('');
-      } else {
-        setError('There was an error submitting feedback, please wait and try again.');
-      }
-    });
-  };
-
-  const showModal = () => {
-    setOpen(true);
-  };
-
-  useEffect(() => {
-    if (!showSuccessMessage) {
-      return () => {};
-    }
-    const timeout = setTimeout(() => {
-      setShowSuccessMessage(false);
-    }, 6000);
-    return () => {
-      clearTimeout(timeout);
-    };
-  }, [showSuccessMessage]);
-
-  useEffect(() => {
-    if (escapePressed) {
-      setOpen(false);
-    }
-  }, [escapePressed]);
-
-  useFocusTrap(dialogRef, open);
-
-  return (
-    <Fragment>
-      <Dialog
-        id="feedbackModal"
-        className={classNames(isDarkTheme ? '__sntry_fdbk_dark' : '', className)}
-        open={open}
-        ref={dialogRef}
-        onClick={handleClose}
-      >
-        <Content onClick={stopPropagation}>
-          <Header>{title}</Header>
-          {errorMessage ? <Error>{errorMessage}</Error> : null}
-          {open && (
-            <FeedbackForm
-              descriptionPlaceholder={descriptionPlaceholder}
-              sendButtonText={sendButtonText}
-              onSubmit={handleSubmit}
-              onClose={handleClose}
-            />
-          )}
-        </Content>
-      </Dialog>
-      <FeedbackSuccessMessage show={showSuccessMessage} />
-      {children({open, showModal})}
-    </Fragment>
-  );
-}
-
-const Dialog = styled('dialog')`
-  --sentry-feedback-bg-color: #fff;
-  --sentry-feedback-bg-hover-color: #f0f0f0;
-  --sentry-feedback-fg-color: #000;
-  --sentry-feedback-border: 1.5px solid rgba(41, 35, 47, 0.13);
-  --sentry-feedback-box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12);
-
-  &.__sntry_fdbk_dark {
-    --sentry-feedback-bg-color: #29232f;
-    --sentry-feedback-bg-hover-color: #3a3540;
-    --sentry-feedback-fg-color: #ebe6ef;
-    --sentry-feedback-border: 1.5px solid rgba(235, 230, 239, 0.15);
-    --sentry-feedback-box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12);
-  }
-
-  background-color: rgba(0, 0, 0, 0.05);
-  border: none;
-  position: fixed;
-  inset: 0;
-  z-index: 10000;
-  width: 100vw;
-  height: 100vh;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  opacity: 1;
-  transition: opacity 0.2s ease-in-out;
-  &:not([open]) {
-    opacity: 0;
-    pointer-events: none;
-    visibility: hidden;
-  }
-`;
-
-const Content = styled('div')`
-  position: fixed;
-  right: 1rem;
-  bottom: 1rem;
-
-  border: var(--sentry-feedback-border);
-  padding: 24px;
-  border-radius: 20px;
-  background-color: var(--sentry-feedback-bg-color);
-  color: var(--sentry-feedback-fg-color);
-
-  width: 320px;
-  max-width: 100%;
-  max-height: calc(100% - 2rem);
-  display: flex;
-  flex-direction: column;
-  box-shadow:
-    0 0 0 1px rgba(0, 0, 0, 0.05),
-    0 4px 16px rgba(0, 0, 0, 0.2);
-  transition: transform 0.2s ease-in-out;
-  transform: translate(0, 0) scale(1);
-  dialog:not([open]) & {
-    transform: translate(0, -16px) scale(0.98);
-  }
-`;
-
-const Header = styled('h2')`
-  font-size: 20px;
-  font-weight: 600;
-  padding: 0;
-  margin: 0;
-  margin-bottom: 16px;
-`;
-
-const Error = styled('div')`
-  color: ${p => p.theme.error};
-  margin-bottom: 16px;
-`;

+ 0 - 72
static/app/components/feedback/widget/feedbackSuccessMessage.tsx

@@ -1,72 +0,0 @@
-import React from 'react';
-import {keyframes} from '@emotion/react';
-import styled from '@emotion/styled';
-
-const Wrapper = styled('div')`
-  position: fixed;
-  width: 100vw;
-  padding: 8px;
-  right: 0;
-  bottom: 0;
-  pointer-events: none;
-  display: flex;
-  justify-content: flex-end;
-  transition: transform 0.4s ease-in-out;
-  transform: translateY(0);
-  z-index: 20000;
-  &[data-hide='true'] {
-    transform: translateY(120%);
-  }
-`;
-
-const borderColor = keyframes`
-  0% {
-    box-shadow: 0 2px 6px rgba(88, 74, 192, 1);
-    border-color: rgba(88, 74, 192, 1);
-  }
-  20% {
-    box-shadow: 0 2px 6px #FFC227;
-    border-color: #FFC227;
-  }
-  40% {
-    box-shadow: 0 2px 6px #FF7738;
-    border-color: #FF7738;
-  }
-  60% {
-    box-shadow: 0 2px 6px #33BF9E;
-    border-color: #33BF9E;
-  }
-  80% {
-    box-shadow: 0 2px 6px #F05781;
-    border-color: #F05781;
-  }
-  100% {
-    box-shadow: 0 2px 6px rgba(88, 74, 192, 1);
-    border-color: rgba(88, 74, 192, 1);
-  }
-`;
-
-const Content = styled('div')`
-  background-color: #fff;
-  border: 2px solid rgba(88, 74, 192, 1);
-  border-radius: 20px;
-  color: rgba(43, 34, 51, 1);
-  font-size: 14px;
-  padding: 6px 24px;
-  box-shadow:
-    0 0 0 1px rgba(0, 0, 0, 0.05),
-    0 4px 16px;
-  animation: ${borderColor} 4s alternate infinite;
-`;
-
-interface FeedbackSuccessMessageProps extends React.HTMLAttributes<HTMLDivElement> {
-  show: boolean;
-}
-
-export function FeedbackSuccessMessage({show, ...props}: FeedbackSuccessMessageProps) {
-  return (
-    <Wrapper data-hide={!show} {...props}>
-      <Content>🎉 Thank you for your feedback! 🙌</Content>
-    </Wrapper>
-  );
-}

+ 18 - 28
static/app/components/feedback/widget/feedbackWidget.tsx

@@ -1,38 +1,28 @@
-import {getCurrentHub} from '@sentry/react';
+import {useEffect} from 'react';
+import {BrowserClient, getCurrentHub} from '@sentry/react';
+import {Feedback} from '@sentry-internal/feedback';
 
 import ConfigStore from 'sentry/stores/configStore';
 import {useLegacyStore} from 'sentry/stores/useLegacyStore';
 
-import {FeedbackButton} from './feedbackButton';
-import {FeedbackModal} from './feedbackModal';
-
-interface FeedbackWidgetProps {
-  title?: string;
-  type?: string;
-}
-
 /**
- * The "Widget" connects the default Feedback button with the Feedback Modal
- *
- * XXX: this is temporary while we make this an SDK feature.
+ * Use this to display the Feedback widget in certain routes/components
  */
-export default function FeedbackWidget({
-  title = 'Report a Bug',
-  type,
-}: FeedbackWidgetProps) {
+export default function FeedbackWidget() {
   const config = useLegacyStore(ConfigStore);
+  const widgetTheme = config.theme === 'dark' ? 'dark' : 'light';
 
-  // Don't render anything if Sentry SDK is not already loaded
-  if (!getCurrentHub()) {
-    return null;
-  }
+  useEffect(() => {
+    const hub = getCurrentHub();
+    const client = hub && hub.getClient<BrowserClient>();
+    const feedback = client?.getIntegration(Feedback);
+    const widget = feedback?.createWidget({
+      colorScheme: widgetTheme,
+    });
+    return () => {
+      feedback?.removeWidget(widget);
+    };
+  }, [widgetTheme]);
 
-  const widgetTheme = config.theme === 'dark' ? 'dark' : 'light';
-  return (
-    <FeedbackModal title={title} type={type} widgetTheme={widgetTheme}>
-      {({open, showModal}) =>
-        open ? null : <FeedbackButton onClick={showModal} widgetTheme={widgetTheme} />
-      }
-    </FeedbackModal>
-  );
+  return null;
 }

+ 0 - 46
static/app/components/feedback/widget/prepareFeedbackEvent.ts

@@ -1,46 +0,0 @@
-import type {Scope} from '@sentry/core';
-import {prepareEvent} from '@sentry/core';
-import type {Client} from '@sentry/types';
-
-import type {FeedbackEvent} from './types';
-
-/**
- * Prepare a feedback event & enrich it with the SDK metadata.
- */
-export async function prepareFeedbackEvent({
-  client,
-  scope,
-  event,
-}: {
-  client: Client;
-  event: FeedbackEvent;
-  scope: Scope;
-}): Promise<FeedbackEvent | null> {
-  const preparedEvent = (await prepareEvent(
-    client.getOptions(),
-    event,
-    {integrations: undefined},
-    scope
-  )) as FeedbackEvent | null;
-
-  // If e.g. a global event processor returned null
-  if (!preparedEvent) {
-    return null;
-  }
-
-  // This normally happens in browser client "_prepareEvent"
-  // but since we do not use this private method from the client, but rather the plain import
-  // we need to do this manually.
-  preparedEvent.platform = preparedEvent.platform || 'javascript';
-
-  // extract the SDK name because `client._prepareEvent` doesn't add it to the event
-  const metadata = client.getSdkMetadata && client.getSdkMetadata();
-  const {name, version} = (metadata && metadata.sdk) || {};
-
-  preparedEvent.sdk = {
-    ...preparedEvent.sdk,
-    name: name || 'sentry.javascript.unknown',
-    version: version || '0.0.0',
-  };
-  return preparedEvent;
-}

+ 0 - 84
static/app/components/feedback/widget/sendFeedbackRequest.ts

@@ -1,84 +0,0 @@
-import {getCurrentHub} from '@sentry/react';
-import {DsnComponents} from '@sentry/types';
-
-import {prepareFeedbackEvent} from './prepareFeedbackEvent';
-
-/**
- * Function taken from sentry-javascript
- */
-function dsnToString(dsn: DsnComponents, withPassword: boolean = false): string {
-  const {host, path, pass, port, projectId, protocol, publicKey} = dsn;
-  return (
-    `${protocol}://${publicKey}${withPassword && pass ? `:${pass}` : ''}` +
-    `@${host}${port ? `:${port}` : ''}/${path ? `${path}/` : path}${projectId}`
-  );
-}
-
-/**
- * Send feedback using `fetch()`
- */
-export async function sendFeedbackRequest({
-  feedback: {message, email, name, replay_id, url},
-  tags,
-}): Promise<Response | null> {
-  const hub = getCurrentHub();
-
-  if (!hub) {
-    return null;
-  }
-
-  const client = hub.getClient();
-  const scope = hub.getScope();
-  const transport = client && client.getTransport();
-  const dsn = client && client.getDsn();
-
-  if (!client || !transport || !dsn) {
-    return null;
-  }
-
-  const baseEvent = {
-    feedback: {
-      contact_email: email,
-      name,
-      message,
-      replay_id,
-      url,
-    },
-    tags,
-    // type: 'feedback_event',
-  };
-
-  const feedbackEvent = await prepareFeedbackEvent({
-    scope,
-    client,
-    event: baseEvent,
-  });
-
-  if (!feedbackEvent) {
-    return null;
-  }
-
-  // Prevent this data (which, if it exists, was used in earlier steps in the processing pipeline) from being sent to
-  // sentry. (Note: Our use of this property comes and goes with whatever we might be debugging, whatever hacks we may
-  // have temporarily added, etc. Even if we don't happen to be using it at some point in the future, let's not get rid
-  // of this `delete`, lest we miss putting it back in the next time the property is in use.)
-  delete feedbackEvent.sdkProcessingMetadata;
-
-  try {
-    const path = 'https://sentry.io/api/0/feedback/';
-    const response = await fetch(path, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-        Authorization: `DSN ${dsnToString(dsn)}`,
-      },
-      body: JSON.stringify(feedbackEvent),
-    });
-    if (!response.ok) {
-      return null;
-    }
-    return response;
-  } catch (err) {
-    return null;
-  }
-}

+ 0 - 48
static/app/components/feedback/widget/useFocusTrap.ts

@@ -1,48 +0,0 @@
-import {useEffect} from 'react';
-
-export const focusableElements = [
-  'input:not([disabled]):not([type="hidden"])',
-  'textarea:not([disabled])',
-  'button:not([disabled])',
-].join(',');
-
-export const useFocusTrap = (ref: React.RefObject<HTMLElement>, autofocus?: boolean) => {
-  useEffect(() => {
-    const element = ref.current;
-    if (!element) {
-      return () => {};
-    }
-    const handleKeyDown = (event: KeyboardEvent) => {
-      const focusable = element.querySelectorAll(focusableElements);
-      const firstFocusable = focusable[0] as HTMLElement;
-      const lastFocusable = focusable[focusable.length - 1] as HTMLElement;
-
-      if (event.key === 'Tab') {
-        if (event.shiftKey) {
-          if (document.activeElement === firstFocusable) {
-            lastFocusable.focus();
-            event.preventDefault();
-          }
-        } else if (document.activeElement === lastFocusable) {
-          firstFocusable.focus();
-          event.preventDefault();
-        }
-      }
-    };
-
-    document.addEventListener('keydown', handleKeyDown);
-
-    return () => {
-      document.removeEventListener('keydown', handleKeyDown);
-    };
-  }, [ref]);
-
-  useEffect(() => {
-    const element = ref.current;
-    if (element && autofocus) {
-      const focusable = element.querySelectorAll(focusableElements);
-      const firstFocusable = focusable[0] as HTMLElement;
-      firstFocusable.focus();
-    }
-  }, [ref, autofocus]);
-};

+ 1 - 1
static/app/views/feedback/index.tsx

@@ -22,7 +22,7 @@ export default function FeedbackContainer({children}: Props) {
       renderDisabled={NoAccess}
     >
       <NoProjectMessage organization={organization}>
-        <FeedbackWidget type="feedback" />
+        <FeedbackWidget />
         {children}
       </NoProjectMessage>
     </Feature>

Some files were not shown because too many files changed in this diff