From 9341608ee3c75a659812a8f44e7398bb070a520a Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 17 Oct 2023 18:48:41 -0400 Subject: [PATCH 01/15] ref: extract widget creation to function, allow handling of multiple widgets --- packages/feedback/src/integration.ts | 376 +++++++----------- packages/feedback/src/sendFeedback.ts | 3 +- packages/feedback/src/types/index.ts | 43 +- packages/feedback/src/widget/Actor.ts | 2 +- packages/feedback/src/widget/Dialog.ts | 14 +- packages/feedback/src/widget/Icon.ts | 4 +- packages/feedback/src/widget/SuccessIcon.ts | 4 +- .../feedback/src/widget/createShadowHost.ts | 35 ++ packages/feedback/src/widget/createWidget.ts | 216 ++++++++++ .../feedback/src/widget/util/createElement.ts | 3 + 10 files changed, 444 insertions(+), 256 deletions(-) create mode 100644 packages/feedback/src/widget/createShadowHost.ts create mode 100644 packages/feedback/src/widget/createWidget.ts diff --git a/packages/feedback/src/integration.ts b/packages/feedback/src/integration.ts index c5ed960a37cc..88f033eb03d3 100644 --- a/packages/feedback/src/integration.ts +++ b/packages/feedback/src/integration.ts @@ -1,4 +1,3 @@ -import { getCurrentHub } from '@sentry/core'; import type { Integration } from '@sentry/types'; import { isBrowser } from '@sentry/utils'; import { logger } from '@sentry/utils'; @@ -17,21 +16,35 @@ import { SUBMIT_BUTTON_LABEL, SUCCESS_MESSAGE_TEXT, } from './constants'; -import type { FeedbackConfigurationWithDefaults, FeedbackFormData, FeedbackTheme } from './types'; -import { handleFeedbackSubmit } from './util/handleFeedbackSubmit'; -import { Actor } from './widget/Actor'; +import type { FeedbackConfigurationWithDefaults, Widget } from './types'; import { createActorStyles } from './widget/Actor.css'; -import { Dialog } from './widget/Dialog'; -import { createDialogStyles } from './widget/Dialog.css'; -import { createMainStyles } from './widget/Main.css'; -import { SuccessMessage } from './widget/SuccessMessage'; +import { createShadowHost } from './widget/createShadowHost'; +import { createWidget } from './widget/createWidget'; +<<<<<<< HEAD interface FeedbackConfiguration extends Partial> { theme?: { dark?: Partial; light?: Partial; }; } +======= +type ElectronProcess = { type?: string }; + +// Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them +function isElectronNodeRenderer(): boolean { + return typeof process !== 'undefined' && (process as ElectronProcess).type === 'renderer'; +} +/** + * Returns true if we are in the browser. + */ +function isBrowser(): boolean { + // eslint-disable-next-line no-restricted-globals + return typeof window !== 'undefined' && (!isNodeEnv() || isElectronNodeRenderer()); +} + +type FeedbackConfiguration = Partial; +>>>>>>> 5fa9a4abb (ref: extract widget creation to function, allow handling of multiple widgets) /** * Feedback integration. When added as an integration to the SDK, it will @@ -55,34 +68,33 @@ export class Feedback implements Integration { public options: FeedbackConfigurationWithDefaults; /** - * Reference to widget actor element (button that opens dialog). + * Reference to widget element that is created when autoInject is true */ - private _actor: ReturnType | null; + private _widget: Widget | null; + /** - * Reference to dialog element + * List of all widgets that are created from the integration */ - private _dialog: ReturnType | null; + private _widgets: Set; + /** * Reference to the host element where widget is inserted */ private _host: HTMLDivElement | null; + /** * Refernce to Shadow DOM root */ private _shadow: ShadowRoot | null; - /** - * State property to track if dialog is currently open - */ - private _isDialogOpen: boolean; /** - * Tracks if dialog has ever been opened at least one time + * Tracks if actor styles have ever been inserted into shadow DOM */ - private _hasDialogEverOpened: boolean; + private _hasInsertedActorStyles: boolean; public constructor({ id = 'sentry-feedback', - attachTo = null, + // attachTo = null, autoInject = true, showEmail = true, showName = true, @@ -94,7 +106,8 @@ export class Feedback implements Integration { isEmailRequired = false, isNameRequired = false, - theme, + themeDark, + themeLight, colorScheme = 'system', buttonLabel = ACTOR_LABEL, @@ -110,22 +123,24 @@ export class Feedback implements Integration { successMessageText = SUCCESS_MESSAGE_TEXT, onActorClick, + onDialogClose, onDialogOpen, onSubmitError, onSubmitSuccess, }: FeedbackConfiguration = {}) { // Initializations this.name = Feedback.id; - this._actor = null; - this._dialog = null; + + // tsc fails if these are not initialized explicitly constructor, e.g. can't call `_initialize()` this._host = null; this._shadow = null; - this._isDialogOpen = false; - this._hasDialogEverOpened = false; + this._widget = null; + this._widgets = new Set(); + this._hasInsertedActorStyles = false; this.options = { id, - attachTo, + // attachTo, autoInject, isAnonymous, isEmailRequired, @@ -135,10 +150,8 @@ export class Feedback implements Integration { useSentryUser, colorScheme, - theme: { - dark: Object.assign({}, DEFAULT_THEME.dark, theme && theme.dark), - light: Object.assign({}, DEFAULT_THEME.light, theme && theme.light), - }, + themeDark: Object.assign({}, DEFAULT_THEME.dark, themeDark), + themeLight: Object.assign({}, DEFAULT_THEME.light, themeLight), buttonLabel, cancelButtonLabel, @@ -153,16 +166,13 @@ export class Feedback implements Integration { successMessageText, onActorClick, + onDialogClose, onDialogOpen, onSubmitError, onSubmitSuccess, }; - - // TOOD: temp for testing; - this.setupOnce(); } - /** If replay has already been initialized */ /** * Setup and initialize replay container */ @@ -176,272 +186,156 @@ export class Feedback implements Integration { if (this._host) { this.remove(); } + // eslint-disable-next-line no-restricted-globals const existingFeedback = document.querySelector(`#${this.options.id}`); if (existingFeedback) { existingFeedback.remove(); } // TODO: End hotloading - const { attachTo, autoInject } = this.options; - if (!attachTo && !autoInject) { - // Nothing to do here - return; - } - - // Setup host element + shadow DOM, if necessary - this._shadow = this._createShadowHost(); - - // If `attachTo` is defined, then attach click handler to it - if (attachTo) { - const actorTarget = - typeof attachTo === 'string' - ? document.querySelector(attachTo) - : typeof attachTo === 'function' - ? attachTo - : null; - - if (!actorTarget) { - logger.warn(`[Feedback] Unable to find element with selector ${actorTarget}`); - return; - } - - actorTarget.addEventListener('click', this._handleActorClick); - } else { - this._createWidgetActor(); - } + const { autoInject } = this.options; - if (!this._host) { - logger.warn('[Feedback] Unable to create host element'); + if (!autoInject) { + // Nothing to do here return; } - document.body.appendChild(this._host); + this._widget = this._createWidget(this.options); } catch (err) { // TODO: error handling - console.error(err); + logger.error(err); } } /** - * Removes the Feedback widget + * Adds click listener to attached element to open a feedback dialog */ - public remove(): void { - if (this._host) { - this._host.remove(); - } - } - - /** - * Opens the Feedback dialog form - */ - public openDialog(): void { + public attachTo(el: Node | string, optionOverrides: Partial): Widget | null { try { - if (this._dialog) { - this._dialog.open(); - this._isDialogOpen = true; - if (this.options.onDialogOpen) { - this.options.onDialogOpen(); - } - return; - } + const options = Object.assign({}, this.options, optionOverrides); - try { - this._shadow = this._createShadowHost(); - } catch { - return; - } + return this._ensureShadowHost(options, ([shadow]) => { + const targetEl = + // eslint-disable-next-line no-restricted-globals + typeof el === 'string' ? document.querySelector(el) : typeof el.addEventListener === 'function' ? el : null; - // Lazy-load until dialog is opened and only inject styles once - if (!this._hasDialogEverOpened) { - this._shadow.appendChild(createDialogStyles(document)); - } + if (!targetEl) { + logger.error('[Feedback] Unable to attach to target element'); + return null; + } - const userKey = this.options.useSentryUser; - const scope = getCurrentHub().getScope(); - const user = scope.getUser(); - - this._dialog = Dialog({ - defaultName: (userKey && user && user[userKey.name]) || '', - defaultEmail: (userKey && user && user[userKey.email]) || '', - onClosed: () => { - this.showActor(); - this._isDialogOpen = false; - }, - onCancel: () => { - this.hideDialog(); - this.showActor(); - }, - onSubmit: this._handleFeedbackSubmit, - options: this.options, + const widget = createWidget({ shadow, options, attachTo: targetEl }); + this._widgets.add(widget); + return widget; }); - this._shadow.appendChild(this._dialog.$el); - - // Hides the default actor whenever dialog is opened - this._actor && this._actor.hide(); - - this._hasDialogEverOpened = true; - if (this.options.onDialogOpen) { - this.options.onDialogOpen(); - } } catch (err) { - // TODO: Error handling? - console.error(err); + logger.error(err); + return null; } } /** - * Hides the dialog + * Creates a new widget. Accepts partial options to override any options passed to constructor. */ - public hideDialog = (): void => { - if (this._dialog) { - this._dialog.close(); - this._isDialogOpen = false; + public createWidget(optionOverrides: Partial): Widget | null { + try { + return this._createWidget(Object.assign({}, this.options, optionOverrides)); + } catch (err) { + logger.error(err); + return null; } - }; + } /** - * Removes the dialog element from DOM + * Removes a single widget */ - public removeDialog = (): void => { - if (this._dialog) { - this._dialog.$el.remove(); - this._dialog = null; + public removeWidget(widget: Widget | null | undefined): boolean { + if (!widget) { + return false; } - }; - /** - * Displays the default actor - */ - public showActor = (): void => { - // TODO: Only show default actor - if (this._actor) { - this._actor.show(); + try { + if (this._widgets.has(widget)) { + widget.removeActor(); + widget.removeDialog(); + this._widgets.delete(widget); + return true; + } + } catch (err) { + logger.error(err); } - }; + return false; + } /** - * Creates the host element of widget's shadow DOM. Returns null if not supported. + * Removes the Feedback integration (including host, shadow DOM, and all widgets) */ - protected _createShadowHost(): ShadowRoot { - if (!document.head.attachShadow) { - // Shadow DOM not supported - logger.warn('[Feedback] Browser does not support shadow DOM API'); - throw new Error('Browser does not support shadow DOM API.'); - } - - // Don't create if it already exists - if (this._shadow) { - return this._shadow; + public remove(): void { + if (this._host) { + this._host.remove(); } - - // Create the host - this._host = document.createElement('div'); - this._host.id = this.options.id; - - // Create the shadow root - const shadow = this._host.attachShadow({ mode: 'open' }); - - shadow.appendChild(createMainStyles(document, this.options.colorScheme, this.options.theme)); - - return shadow; + this._initialize(); } /** - * Creates the host element of our shadow DOM as well as the actor + * Initializes values of protected properties */ - protected _createWidgetActor(): void { - if (!this._shadow) { - // This shouldn't happen... we could call `_createShadowHost` if this is the case? - return; - } - - try { - this._shadow.appendChild(createActorStyles(document)); - - // Create Actor component - this._actor = Actor({ options: this.options, onClick: this._handleActorClick }); - - this._shadow.appendChild(this._actor.$el); - } catch (err) { - // TODO: error handling - console.error(err); - } + protected _initialize(): void { + this._host = null; + this._shadow = null; + this._widget = null; + this._widgets = new Set(); + this._hasInsertedActorStyles = false; } /** - * Show the success message for 5 seconds + * Creates a new widget, after ensuring shadow DOM exists */ - protected _showSuccessMessage(): void { - if (!this._shadow) { - return; - } - - try { - const success = SuccessMessage({ - message: this.options.successMessageText, - onRemove: () => { - if (timeoutId) { - clearTimeout(timeoutId); - } - this.showActor(); - }, - }); - - this._shadow.appendChild(success.$el); + protected _createWidget(options: FeedbackConfigurationWithDefaults): Widget | null { + return this._ensureShadowHost(options, ([shadow]) => { + const widget = createWidget({ shadow, options }); + + if (!this._hasInsertedActorStyles && widget.actor) { + // eslint-disable-next-line no-restricted-globals + shadow.appendChild(createActorStyles(document)); + this._hasInsertedActorStyles = true; + } - const timeoutId = setTimeout(() => { - if (success) { - success.remove(); - } - }, 5000); - } catch (err) { - // TODO: error handling - console.error(err); - } + this._widgets.add(widget); + return widget; + }); } /** - * Handles when the actor is clicked, opens the dialog modal and calls any - * callbacks. + * Ensures that shadow DOM exists and is added to the DOM */ - protected _handleActorClick = (): void => { - // Open dialog - if (!this._isDialogOpen) { - this.openDialog(); - } + protected _ensureShadowHost( + options: FeedbackConfigurationWithDefaults, + cb: (createShadowHostResult: ReturnType) => T, + ): T | null { + let needsAppendHost = false; - // Hide actor button - if (this._actor) { - this._actor.hide(); + // Don't create if it already exists + if (!this._shadow && !this._host) { + const [shadow, host] = createShadowHost({ options }); + this._shadow = shadow; + this._host = host; + needsAppendHost = true; } - if (this.options.onActorClick) { - this.options.onActorClick(); + if (!this._shadow || !this._host) { + logger.warn('[Feedback] Unable to create host element and/or shadow DOM'); + // This shouldn't happen + return null; } - }; - - /** - * Handler for when the feedback form is completed by the user. This will - * create and send the feedback message as an event. - */ - protected _handleFeedbackSubmit = async (feedback: FeedbackFormData): Promise => { - const result = await handleFeedbackSubmit(this._dialog, feedback); - // Error submitting feedback - if (!result) { - if (this.options.onSubmitError) { - this.options.onSubmitError(); - } + const result = cb([this._shadow, this._host]); - return; + if (needsAppendHost) { + // eslint-disable-next-line no-restricted-globals + document.body.appendChild(this._host); } - // Success - this.removeDialog(); - this._showSuccessMessage(); - - if (this.options.onSubmitSuccess) { - this.options.onSubmitSuccess(); - } - }; + return result; + } } diff --git a/packages/feedback/src/sendFeedback.ts b/packages/feedback/src/sendFeedback.ts index 626f4baba735..267805007403 100644 --- a/packages/feedback/src/sendFeedback.ts +++ b/packages/feedback/src/sendFeedback.ts @@ -1,5 +1,6 @@ import type { BrowserClient, Replay } from '@sentry/browser'; import { getCurrentHub } from '@sentry/core'; +import { getLocationHref } from '@sentry/utils'; import { sendFeedbackRequest } from './util/sendFeedbackRequest'; @@ -18,7 +19,7 @@ interface SendFeedbackOptions { * Public API to send a Feedback item to Sentry */ export function sendFeedback( - { name, email, message, url = document.location.href }: SendFeedbackParams, + { name, email, message, url = getLocationHref() }: SendFeedbackParams, { includeReplay = true }: SendFeedbackOptions = {}, ): ReturnType { const hub = getCurrentHub(); diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts index 267fa3465d2a..2a2596349645 100644 --- a/packages/feedback/src/types/index.ts +++ b/packages/feedback/src/types/index.ts @@ -1,5 +1,8 @@ import type { Event, Primitive } from '@sentry/types'; +import type { ActorComponent } from '../widget/Actor'; +import type { DialogComponent } from '../widget/Dialog'; + export type SentryTags = { [key: string]: Primitive } | undefined; /** @@ -40,11 +43,6 @@ export interface FeedbackConfigurationWithDefaults { */ id: string; - /** - * DOM Selector to attach click listener to, for opening Feedback dialog. - */ - attachTo: Node | string | null; - /** * Auto-inject default Feedback actor button to the DOM when integration is * added. @@ -92,12 +90,13 @@ export interface FeedbackConfigurationWithDefaults { colorScheme: 'system' | 'light' | 'dark'; /** - * Theme customization, will be merged with default theme values. + * Light theme customization, will be merged with default theme values. */ - theme: { - dark: FeedbackTheme; - light: FeedbackTheme; - }; + themeLight: FeedbackTheme; + /** + * Dark theme customization, will be merged with default theme values. + */ + themeDark: FeedbackTheme; // * End of Color theme customization * // // * Text customization * // @@ -148,6 +147,11 @@ export interface FeedbackConfigurationWithDefaults { // * End of text customization * // // * Start of Callbacks * // + /** + * Callback when dialog is closed + */ + onDialogClose?: () => void; + /** * Callback when dialog is opened */ @@ -217,3 +221,22 @@ export interface FeedbackThemes { export interface FeedbackComponent { $el: T; } + +/** + * A widget consists of: + * - actor button [that opens dialog] + * - dialog + feedback form + * - shadow root? + */ +export interface Widget { + actor: ActorComponent | undefined; + dialog: DialogComponent | undefined; + + showActor: () => void; + hideActor: () => void; + removeActor: () => void; + + openDialog: () => void; + hideDialog: () => void; + removeDialog: () => void; +} diff --git a/packages/feedback/src/widget/Actor.ts b/packages/feedback/src/widget/Actor.ts index 5610905ed3ee..ddd2081a3732 100644 --- a/packages/feedback/src/widget/Actor.ts +++ b/packages/feedback/src/widget/Actor.ts @@ -7,7 +7,7 @@ interface Props { onClick?: (e: MouseEvent) => void; } -interface ActorComponent extends FeedbackComponent { +export interface ActorComponent extends FeedbackComponent { /** * Shows the actor element */ diff --git a/packages/feedback/src/widget/Dialog.ts b/packages/feedback/src/widget/Dialog.ts index 5e37a74239f6..135d31ae2e17 100644 --- a/packages/feedback/src/widget/Dialog.ts +++ b/packages/feedback/src/widget/Dialog.ts @@ -41,6 +41,11 @@ export interface DialogComponent extends FeedbackComponent { * Closes the dialog and form */ close: () => void; + + /** + * Check if dialog is currently opened + */ + checkIsOpen: () => boolean; } /** @@ -87,6 +92,13 @@ export function Dialog({ } } + /** + * Check if dialog is currently opened + */ + function checkIsOpen(): boolean { + return ($el && $el.open === true) || false; + } + const { $el: $form, setSubmitEnabled, @@ -104,7 +116,6 @@ export function Dialog({ $el = h( 'dialog', { - id: 'feedback-dialog', className: 'dialog', open: true, onClick: handleDialogClick, @@ -131,5 +142,6 @@ export function Dialog({ setSubmitEnabled, open, close, + checkIsOpen, }; } diff --git a/packages/feedback/src/widget/Icon.ts b/packages/feedback/src/widget/Icon.ts index e0564dd92b69..5a305cdb3d64 100644 --- a/packages/feedback/src/widget/Icon.ts +++ b/packages/feedback/src/widget/Icon.ts @@ -11,7 +11,9 @@ interface IconReturn { * Feedback Icon */ export function Icon(): IconReturn { - const cENS = document.createElementNS.bind(document, XMLNS); + const cENS = (tagName: K): SVGElementTagNameMap[K] => + // eslint-disable-next-line no-restricted-globals + document.createElementNS(XMLNS, tagName); const svg = setAttributesNS(cENS('svg'), { class: 'feedback-icon', width: `${SIZE}`, diff --git a/packages/feedback/src/widget/SuccessIcon.ts b/packages/feedback/src/widget/SuccessIcon.ts index bd60d8306271..a21307ccfd31 100644 --- a/packages/feedback/src/widget/SuccessIcon.ts +++ b/packages/feedback/src/widget/SuccessIcon.ts @@ -12,7 +12,9 @@ interface IconReturn { * Success Icon (checkmark) */ export function SuccessIcon(): IconReturn { - const cENS = document.createElementNS.bind(document, XMLNS); + const cENS = (tagName: K): SVGElementTagNameMap[K] => + // eslint-disable-next-line no-restricted-globals + document.createElementNS(XMLNS, tagName); const svg = setAttributesNS(cENS('svg'), { class: 'success-icon', width: `${WIDTH}`, diff --git a/packages/feedback/src/widget/createShadowHost.ts b/packages/feedback/src/widget/createShadowHost.ts new file mode 100644 index 000000000000..d75dc9020db5 --- /dev/null +++ b/packages/feedback/src/widget/createShadowHost.ts @@ -0,0 +1,35 @@ +import { logger } from '@sentry/utils'; + +import type { FeedbackConfigurationWithDefaults } from '../types'; +import { createDialogStyles } from './Dialog.css'; +import { createMainStyles } from './Main.css'; + +interface CreateShadowHostParams { + options: FeedbackConfigurationWithDefaults; +} +/** + * + */ +export function createShadowHost({ options }: CreateShadowHostParams): [shadow: ShadowRoot, host: HTMLDivElement] { + // eslint-disable-next-line no-restricted-globals + const doc = document; + if (!doc.head.attachShadow) { + // Shadow DOM not supported + logger.warn('[Feedback] Browser does not support shadow DOM API'); + throw new Error('Browser does not support shadow DOM API.'); + } + + // Create the host + const host = doc.createElement('div'); + host.id = options.id; + + // Create the shadow root + const shadow = host.attachShadow({ mode: 'open' }); + + shadow.appendChild( + createMainStyles(doc, options.colorScheme, { dark: options.themeDark, light: options.themeLight }), + ); + shadow.appendChild(createDialogStyles(doc)); + + return [shadow, host]; +} diff --git a/packages/feedback/src/widget/createWidget.ts b/packages/feedback/src/widget/createWidget.ts new file mode 100644 index 000000000000..5ef6596d522e --- /dev/null +++ b/packages/feedback/src/widget/createWidget.ts @@ -0,0 +1,216 @@ +import { getCurrentHub } from '@sentry/core'; +import { logger } from '@sentry/utils'; + +import type { FeedbackConfigurationWithDefaults, FeedbackFormData, Widget } from '../types'; +import { handleFeedbackSubmit } from '../util/handleFeedbackSubmit'; +import type { ActorComponent } from './Actor'; +import { Actor } from './Actor'; +import type { DialogComponent } from './Dialog'; +import { Dialog } from './Dialog'; +import { SuccessMessage } from './SuccessMessage'; + +interface CreateWidgetParams { + shadow: ShadowRoot; + options: FeedbackConfigurationWithDefaults; + attachTo?: Node; +} + +/** + * + */ +export function createWidget({ shadow, options, attachTo }: CreateWidgetParams): Widget { + let actor: ActorComponent | undefined; + let dialog: DialogComponent | undefined; + let isDialogOpen: boolean = false; + + /** + * Show the success message for 5 seconds + */ + function showSuccessMessage(): void { + if (!shadow) { + return; + } + + try { + const success = SuccessMessage({ + message: options.successMessageText, + onRemove: () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + showActor(); + }, + }); + + shadow.appendChild(success.$el); + + const timeoutId = setTimeout(() => { + if (success) { + success.remove(); + } + }, 5000); + } catch (err) { + // TODO: error handling + logger.error(err); + } + } + + /** + * Handler for when the feedback form is completed by the user. This will + * create and send the feedback message as an event. + */ + async function _handleFeedbackSubmit(feedback: FeedbackFormData): Promise { + if (!dialog) { + return; + } + + const result = await handleFeedbackSubmit(dialog, feedback); + + // Error submitting feedback + if (!result) { + if (options.onSubmitError) { + options.onSubmitError(); + } + + return; + } + + // Success + removeDialog(); + showSuccessMessage(); + + if (options.onSubmitSuccess) { + options.onSubmitSuccess(); + } + } + + /** + * Displays the default actor + */ + function showActor(): void { + actor && actor.show(); + } + + /** + * Hides the default actor + */ + function hideActor(): void { + actor && actor.hide(); + } + + /** + * Removes the default actor element + */ + function removeActor(): void { + actor && actor.$el.remove(); + } + + /** + * + */ + function openDialog(): void { + try { + if (dialog) { + dialog.open(); + isDialogOpen = true; + if (options.onDialogOpen) { + options.onDialogOpen(); + } + return; + } + + const userKey = !options.isAnonymous && options.useSentryUser; + const scope = getCurrentHub().getScope(); + const user = scope && scope.getUser(); + + dialog = Dialog({ + defaultName: (userKey && user && user[userKey.name]) || '', + defaultEmail: (userKey && user && user[userKey.email]) || '', + onClosed: () => { + showActor(); + isDialogOpen = false; + }, + onCancel: () => { + hideDialog(); + showActor(); + }, + onSubmit: _handleFeedbackSubmit, + options, + }); + + shadow.appendChild(dialog.$el); + + // Hides the default actor whenever dialog is opened + hideActor(); + + if (options.onDialogOpen) { + options.onDialogOpen(); + } + } catch (err) { + // TODO: Error handling? + logger.error(err); + } + } + + /** + * Hides the dialog + */ + function hideDialog(): void { + if (dialog) { + dialog.close(); + isDialogOpen = false; + + if (options.onDialogClose) { + options.onDialogClose(); + } + } + } + + /** + * Removes the dialog element from DOM + */ + function removeDialog(): void { + if (dialog) { + hideDialog(); + dialog.$el.remove(); + dialog = undefined; + } + } + + /** + * + */ + function handleActorClick(): void { + // Open dialog + if (!isDialogOpen) { + openDialog(); + } + + // Hide actor button + hideActor(); + + if (options.onActorClick) { + options.onActorClick(); + } + } + + if (!attachTo) { + actor = Actor({ options, onClick: handleActorClick }); + shadow.appendChild(actor.$el); + } else { + attachTo.addEventListener('click', handleActorClick); + } + + return { + actor, + dialog, + + showActor, + hideActor, + removeActor, + + openDialog, + hideDialog, + removeDialog, + }; +} diff --git a/packages/feedback/src/widget/util/createElement.ts b/packages/feedback/src/widget/util/createElement.ts index c9760be7e3db..4d9849a19f54 100644 --- a/packages/feedback/src/widget/util/createElement.ts +++ b/packages/feedback/src/widget/util/createElement.ts @@ -7,6 +7,7 @@ export function createElement( attributes: { [key: string]: string | boolean | EventListenerOrEventListenerObject } | null, ...children: any ): HTMLElementTagNameMap[K] { + // eslint-disable-next-line no-restricted-globals const element = document.createElement(tagName); if (attributes) { @@ -42,10 +43,12 @@ function appendChild(parent: Node, child: any): void { } else if (child === false) { // do nothing if child evaluated to false } else if (typeof child === 'string') { + // eslint-disable-next-line no-restricted-globals parent.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { parent.appendChild(child); } else { + // eslint-disable-next-line no-restricted-globals parent.appendChild(document.createTextNode(String(child))); } } From 18a6016027cbf454e1c8d279367433048eb91bc1 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 17 Oct 2023 18:59:28 -0400 Subject: [PATCH 02/15] update versions --- packages/feedback/package.json | 11 +++---- yarn.lock | 53 ---------------------------------- 2 files changed, 6 insertions(+), 58 deletions(-) diff --git a/packages/feedback/package.json b/packages/feedback/package.json index e56a2aa09613..0f8a6f9f9eea 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "7.75.1", + "version": "0.0.1-alpha", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", @@ -23,10 +23,11 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.70.0", - "@sentry/core": "7.70.0", - "@sentry/types": "7.70.0", - "@sentry/utils": "7.70.0" + "@sentry/browser": "7.74.1", + "@sentry/core": "7.74.1", + "@sentry/types": "7.74.1", + "@sentry/utils": "7.74.1", + "tslib": "^2.4.1 || ^1.9.3" }, "scripts": { "build": "run-p build:transpile build:types build:bundle", diff --git a/yarn.lock b/yarn.lock index 9f19615e7308..64e376067cc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4958,28 +4958,6 @@ fflate "^0.4.4" mitt "^3.0.0" -"@sentry-internal/tracing@7.70.0": - version "7.70.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.70.0.tgz#00fd30426a6d4737385004434a39cf60736beafc" - integrity sha512-SpbE6wZhs6QwG2ORWCt8r28o1T949qkWx/KeRTCdK4Ub95PQ3Y3DgnqD8Wz//3q50Wt6EZDEibmz4t067g6PPg== - dependencies: - "@sentry/core" "7.70.0" - "@sentry/types" "7.70.0" - "@sentry/utils" "7.70.0" - tslib "^2.4.1 || ^1.9.3" - -"@sentry/browser@7.70.0": - version "7.70.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.70.0.tgz#e284999843bebc5bccd2c7b247f01aa048518f5c" - integrity sha512-PB+IP49/TLcnDHCj9eJ5tcHE0pzXg23wBakmF3KGMSd5nxEbUvmOsaFPZcgUUlL9JlU3v1Y40We7HdPStrY6oA== - dependencies: - "@sentry-internal/tracing" "7.70.0" - "@sentry/core" "7.70.0" - "@sentry/replay" "7.70.0" - "@sentry/types" "7.70.0" - "@sentry/utils" "7.70.0" - tslib "^2.4.1 || ^1.9.3" - "@sentry/bundler-plugin-core@0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-0.6.1.tgz#6c6a2ff3cdc98cd0ff1c30c59408cee9f067adf2" @@ -5054,37 +5032,6 @@ proxy-from-env "^1.1.0" which "^2.0.2" -"@sentry/core@7.70.0": - version "7.70.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.70.0.tgz#c481ef27cf05293fb681ee4ff4d4b0b1e8664bb5" - integrity sha512-voUsGVM+jwRp99AQYFnRvr7sVd2tUhIMj1L6F42LtD3vp7t5ZnKp3NpXagtFW2vWzXESfyJUBhM0qI/bFvn7ZA== - dependencies: - "@sentry/types" "7.70.0" - "@sentry/utils" "7.70.0" - tslib "^2.4.1 || ^1.9.3" - -"@sentry/replay@7.70.0": - version "7.70.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.70.0.tgz#fd0c75cb0d632e15c8d270af33cb157d328399e8" - integrity sha512-XjnyE6ORREz9kBWWHdXaIjS9P2Wo7uEw+y23vfLQwzV0Nx3xJ+FG4dwf8onyIoeCZDKbz7cqQIbugU1gkgUtZw== - dependencies: - "@sentry/core" "7.70.0" - "@sentry/types" "7.70.0" - "@sentry/utils" "7.70.0" - -"@sentry/types@7.70.0": - version "7.70.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.70.0.tgz#c7b533bb18144e3b020550b38cf4812c32d05ffe" - integrity sha512-rY4DqpiDBtXSk4MDNBH3dwWqfPbNBI/9GA7Y5WJSIcObBtfBKp0fzYliHJZD0pgM7d4DPFrDn42K9Iiumgymkw== - -"@sentry/utils@7.70.0": - version "7.70.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.70.0.tgz#825387ceb10cbb1e145357394b697a1a6d60eb74" - integrity sha512-0cChMH0lsGp+5I3D4wOHWwjFN19HVrGUs7iWTLTO5St3EaVbdeLbI1vFXHxMxvopbwgpeZafbreHw/loIdZKpw== - dependencies: - "@sentry/types" "7.70.0" - tslib "^2.4.1 || ^1.9.3" - "@sentry/vite-plugin@^0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-0.6.1.tgz#31eb744e8d87b1528eed8d41433647727a62e7c0" From b1f8c050df8e936e59b5bcbbaadfc622223add14 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 17 Oct 2023 18:59:57 -0400 Subject: [PATCH 03/15] v0.0.1-alpha.0 --- packages/feedback/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 0f8a6f9f9eea..58c296ef540b 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "0.0.1-alpha", + "version": "0.0.1-alpha.0", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", From f89f3613bdefb676f1831ab76ffa311345f8fc3f Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 18 Oct 2023 15:23:23 -0400 Subject: [PATCH 04/15] add css for z-index --- packages/feedback/src/widget/Main.css.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/feedback/src/widget/Main.css.ts b/packages/feedback/src/widget/Main.css.ts index 9932fdcfa771..73fd1b721930 100644 --- a/packages/feedback/src/widget/Main.css.ts +++ b/packages/feedback/src/widget/Main.css.ts @@ -16,6 +16,7 @@ export function createMainStyles( --right: 1rem; --top: auto; --left: auto; + --zIndex: 100000; position: fixed; left: var(--left); @@ -23,6 +24,8 @@ export function createMainStyles( bottom: var(--bottom); top: var(--top); + z-index: var(--zIndex); + font-family: ${theme.fontFamily}; font-size: ${theme.fontSize}; --bg-color: ${theme.background}; From 74b6df9fa596bc374469e463fcfa79ddeb5f5cf2 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 18 Oct 2023 15:26:39 -0400 Subject: [PATCH 05/15] v0.0.1-alpha.1 --- packages/feedback/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 58c296ef540b..63ed03033c87 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "0.0.1-alpha.0", + "version": "0.0.1-alpha.1", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", From 277f38739a8d22cc1c5d66aef6fe815ae95015a6 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 18 Oct 2023 16:15:50 -0400 Subject: [PATCH 06/15] fix dark mode --- packages/feedback/src/integration.ts | 3 +++ packages/feedback/src/widget/Main.css.ts | 34 ++++++++++++++++-------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/feedback/src/integration.ts b/packages/feedback/src/integration.ts index 88f033eb03d3..bba2a6b871e5 100644 --- a/packages/feedback/src/integration.ts +++ b/packages/feedback/src/integration.ts @@ -329,6 +329,9 @@ export class Feedback implements Integration { return null; } + // set data attribute on host for different themes + this._host.dataset.sentryFeedbackColorscheme = options.colorScheme; + const result = cb([this._shadow, this._host]); if (needsAppendHost) { diff --git a/packages/feedback/src/widget/Main.css.ts b/packages/feedback/src/widget/Main.css.ts index 73fd1b721930..53876c77ca03 100644 --- a/packages/feedback/src/widget/Main.css.ts +++ b/packages/feedback/src/widget/Main.css.ts @@ -9,7 +9,6 @@ export function createMainStyles( themes: FeedbackThemes, ): HTMLStyleElement { const style = d.createElement('style'); - const theme = colorScheme === 'system' ? themes.light : themes[colorScheme]; style.textContent = ` :host { --bottom: 1rem; @@ -26,16 +25,17 @@ export function createMainStyles( z-index: var(--zIndex); - font-family: ${theme.fontFamily}; - font-size: ${theme.fontSize}; - --bg-color: ${theme.background}; - --bg-hover-color: ${theme.backgroundHover}; - --fg-color: ${theme.foreground}; - --error-color: ${theme.error}; - --success-color: ${theme.success}; - --border: ${theme.border}; - --box-shadow: ${theme.boxShadow}; + font-family: ${themes.light.fontFamily}; + font-size: ${themes.light.fontSize}; + --bg-color: ${themes.light.background}; + --bg-hover-color: ${themes.light.backgroundHover}; + --fg-color: ${themes.light.foreground}; + --error-color: ${themes.light.error}; + --success-color: ${themes.light.success}; + --border: ${themes.light.border}; + --box-shadow: ${themes.light.boxShadow}; } + ${ colorScheme === 'system' ? ` @@ -51,7 +51,19 @@ ${ } } ` - : '' + : ` +:host-context([data-sentry-feedback-colorscheme="dark"]) { + font-family: ${themes.dark.fontFamily}; + font-size: ${themes.dark.fontSize}; + --bg-color: ${themes.dark.background}; + --bg-hover-color: ${themes.dark.backgroundHover}; + --fg-color: ${themes.dark.foreground}; + --error-color: ${themes.dark.error}; + --success-color: ${themes.dark.success}; + --border: ${themes.dark.border}; + --box-shadow: ${themes.dark.boxShadow}; +} +` }`; return style; From b3c6a0630e7962a09935a627f8c4a0f7835ddcd2 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 18 Oct 2023 16:21:40 -0400 Subject: [PATCH 07/15] v0.0.1-alpha.2 --- packages/feedback/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 63ed03033c87..4e4e237df194 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "0.0.1-alpha.1", + "version": "0.0.1-alpha.2", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", From 2be4124e4519355c10a1ca04460b5c6631794af7 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 18 Oct 2023 17:56:53 -0400 Subject: [PATCH 08/15] rename css vars --- packages/feedback/src/widget/Main.css.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/feedback/src/widget/Main.css.ts b/packages/feedback/src/widget/Main.css.ts index 53876c77ca03..e434739dcecd 100644 --- a/packages/feedback/src/widget/Main.css.ts +++ b/packages/feedback/src/widget/Main.css.ts @@ -15,18 +15,20 @@ export function createMainStyles( --right: 1rem; --top: auto; --left: auto; - --zIndex: 100000; + --z-index: 100000; + --font-family: ${themes.light.fontFamily}; + --font-size: ${themes.light.fontSize}; position: fixed; left: var(--left); right: var(--right); bottom: var(--bottom); top: var(--top); + z-index: var(--z-index); - z-index: var(--zIndex); + font-family: var(--font-family); + font-size: var(--font-size); - font-family: ${themes.light.fontFamily}; - font-size: ${themes.light.fontSize}; --bg-color: ${themes.light.background}; --bg-hover-color: ${themes.light.backgroundHover}; --fg-color: ${themes.light.foreground}; @@ -48,13 +50,13 @@ ${ --success-color: ${themes.dark.success}; --border: ${themes.dark.border}; --box-shadow: ${themes.dark.boxShadow}; + --font-family: ${themes.dark.fontFamily}; + --font-size: ${themes.dark.fontSize}; } } ` : ` :host-context([data-sentry-feedback-colorscheme="dark"]) { - font-family: ${themes.dark.fontFamily}; - font-size: ${themes.dark.fontSize}; --bg-color: ${themes.dark.background}; --bg-hover-color: ${themes.dark.backgroundHover}; --fg-color: ${themes.dark.foreground}; @@ -62,6 +64,8 @@ ${ --success-color: ${themes.dark.success}; --border: ${themes.dark.border}; --box-shadow: ${themes.dark.boxShadow}; + --font-family: ${themes.dark.fontFamily}; + --font-size: ${themes.dark.fontSize}; } ` }`; From b64d1b5072930613a6cbdc9255cd227df69f5bef Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 24 Oct 2023 11:53:50 -0400 Subject: [PATCH 09/15] update version --- packages/feedback/package.json | 9 ++++----- yarn.lock | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 4e4e237df194..a3dab9195987 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -23,11 +23,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.74.1", - "@sentry/core": "7.74.1", - "@sentry/types": "7.74.1", - "@sentry/utils": "7.74.1", - "tslib": "^2.4.1 || ^1.9.3" + "@sentry/browser": "7.75.0", + "@sentry/core": "7.75.0", + "@sentry/types": "7.75.0", + "@sentry/utils": "7.75.0" }, "scripts": { "build": "run-p build:transpile build:types build:bundle", diff --git a/yarn.lock b/yarn.lock index 64e376067cc6..5927215390a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29543,7 +29543,7 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, "tslib@^2.4.1 || ^1.9.3": +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1: version "2.5.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== From 7cce102de79e5813a0f3b87b8f872c9d157da5b4 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 24 Oct 2023 11:57:07 -0400 Subject: [PATCH 10/15] v0.0.1-alpha.3 --- packages/feedback/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/feedback/package.json b/packages/feedback/package.json index a3dab9195987..d9af3933436a 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "0.0.1-alpha.2", + "version": "0.0.1-alpha.3", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", From e61571fc37082b3426007a874f56f264a275c683 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 24 Oct 2023 12:35:52 -0400 Subject: [PATCH 11/15] v0.0.1-alpha.4 --- packages/feedback/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/feedback/package.json b/packages/feedback/package.json index d9af3933436a..3164094b2838 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "0.0.1-alpha.3", + "version": "0.0.1-alpha.4", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", From 413ee5aa9b9016efeea144cb5c892be1d0d94266 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 24 Oct 2023 17:59:09 -0400 Subject: [PATCH 12/15] feat: add `referer` option when calling attachTo/createWidget --- packages/feedback/src/integration.ts | 8 +++----- packages/feedback/src/sendFeedback.ts | 8 +++----- packages/feedback/src/types/index.ts | 20 +++++++++++++++++++ .../feedback/src/util/handleFeedbackSubmit.ts | 7 ++++--- .../feedback/src/util/sendFeedbackRequest.ts | 4 ++++ packages/feedback/src/widget/createWidget.ts | 6 +++--- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/feedback/src/integration.ts b/packages/feedback/src/integration.ts index bba2a6b871e5..bbb491a4daac 100644 --- a/packages/feedback/src/integration.ts +++ b/packages/feedback/src/integration.ts @@ -16,7 +16,7 @@ import { SUBMIT_BUTTON_LABEL, SUCCESS_MESSAGE_TEXT, } from './constants'; -import type { FeedbackConfigurationWithDefaults, Widget } from './types'; +import type { CreateWidgetOptionOverrides, FeedbackConfigurationWithDefaults, Widget } from './types'; import { createActorStyles } from './widget/Actor.css'; import { createShadowHost } from './widget/createShadowHost'; import { createWidget } from './widget/createWidget'; @@ -94,7 +94,6 @@ export class Feedback implements Integration { public constructor({ id = 'sentry-feedback', - // attachTo = null, autoInject = true, showEmail = true, showName = true, @@ -140,7 +139,6 @@ export class Feedback implements Integration { this.options = { id, - // attachTo, autoInject, isAnonymous, isEmailRequired, @@ -210,7 +208,7 @@ export class Feedback implements Integration { /** * Adds click listener to attached element to open a feedback dialog */ - public attachTo(el: Node | string, optionOverrides: Partial): Widget | null { + public attachTo(el: Node | string, optionOverrides: CreateWidgetOptionOverrides): Widget | null { try { const options = Object.assign({}, this.options, optionOverrides); @@ -237,7 +235,7 @@ export class Feedback implements Integration { /** * Creates a new widget. Accepts partial options to override any options passed to constructor. */ - public createWidget(optionOverrides: Partial): Widget | null { + public createWidget(optionOverrides: CreateWidgetOptionOverrides): Widget | null { try { return this._createWidget(Object.assign({}, this.options, optionOverrides)); } catch (err) { diff --git a/packages/feedback/src/sendFeedback.ts b/packages/feedback/src/sendFeedback.ts index 267805007403..133fcbcc7b92 100644 --- a/packages/feedback/src/sendFeedback.ts +++ b/packages/feedback/src/sendFeedback.ts @@ -3,6 +3,7 @@ import { getCurrentHub } from '@sentry/core'; import { getLocationHref } from '@sentry/utils'; import { sendFeedbackRequest } from './util/sendFeedbackRequest'; +import { SendFeedbackOptions } from './types'; interface SendFeedbackParams { message: string; @@ -11,16 +12,12 @@ interface SendFeedbackParams { url?: string; } -interface SendFeedbackOptions { - includeReplay?: boolean; -} - /** * Public API to send a Feedback item to Sentry */ export function sendFeedback( { name, email, message, url = getLocationHref() }: SendFeedbackParams, - { includeReplay = true }: SendFeedbackOptions = {}, + { referrer, includeReplay = true }: SendFeedbackOptions = {}, ): ReturnType { const hub = getCurrentHub(); const client = hub && hub.getClient(); @@ -38,5 +35,6 @@ export function sendFeedback( url, replay_id: replayId, }, + referrer, }); } diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts index 2a2596349645..a1cc9686c324 100644 --- a/packages/feedback/src/types/index.ts +++ b/packages/feedback/src/types/index.ts @@ -29,8 +29,24 @@ export interface SendFeedbackData { replay_id?: string; name?: string; }; + referrer?: string; } +export interface SendFeedbackOptions { + /** + * Should include replay with the feedback? + */ + includeReplay?: boolean; + + /** + * Allows user to set a referrer for feedback, to act as a category for the feedback + */ + referrer?: string; +} + +/** + * Feedback data expected from UI/form + */ export interface FeedbackFormData { message: string; email?: string; @@ -213,6 +229,10 @@ export interface FeedbackTheme { error: string; } +export interface CreateWidgetOptionOverrides extends Partial { + referrer?: string; +} + export interface FeedbackThemes { dark: FeedbackTheme; light: FeedbackTheme; diff --git a/packages/feedback/src/util/handleFeedbackSubmit.ts b/packages/feedback/src/util/handleFeedbackSubmit.ts index c109c0c9214e..16efd1e568c3 100644 --- a/packages/feedback/src/util/handleFeedbackSubmit.ts +++ b/packages/feedback/src/util/handleFeedbackSubmit.ts @@ -1,13 +1,14 @@ import { sendFeedback } from '../sendFeedback'; -import type { FeedbackFormData } from '../types'; +import type { FeedbackFormData, SendFeedbackOptions } from '../types'; import type { DialogComponent } from '../widget/Dialog'; /** - * + * Calls `sendFeedback` to send feedback, handles UI behavior of dialog. */ export async function handleFeedbackSubmit( dialog: DialogComponent | null, feedback: FeedbackFormData, + options?: SendFeedbackOptions ): Promise { if (!dialog) { // Not sure when this would happen @@ -25,7 +26,7 @@ export async function handleFeedbackSubmit( try { dialog.hideError(); dialog.setSubmitDisabled(); - const resp = await sendFeedback(feedback); + const resp = await sendFeedback(feedback, options); if (!resp) { // Errored... re-enable submit button diff --git a/packages/feedback/src/util/sendFeedbackRequest.ts b/packages/feedback/src/util/sendFeedbackRequest.ts index 626457d6122b..60d99066d250 100644 --- a/packages/feedback/src/util/sendFeedbackRequest.ts +++ b/packages/feedback/src/util/sendFeedbackRequest.ts @@ -9,6 +9,7 @@ import { prepareFeedbackEvent } from './prepareFeedbackEvent'; */ export async function sendFeedbackRequest({ feedback: { message, email, name, replay_id, url }, + referrer, }: SendFeedbackData): Promise { const hub = getCurrentHub(); @@ -33,6 +34,9 @@ export async function sendFeedbackRequest({ replay_id, url, }, + tags: { + referrer, + } // type: 'feedback_event', }; diff --git a/packages/feedback/src/widget/createWidget.ts b/packages/feedback/src/widget/createWidget.ts index 5ef6596d522e..6246dd27ff6f 100644 --- a/packages/feedback/src/widget/createWidget.ts +++ b/packages/feedback/src/widget/createWidget.ts @@ -11,12 +11,12 @@ import { SuccessMessage } from './SuccessMessage'; interface CreateWidgetParams { shadow: ShadowRoot; - options: FeedbackConfigurationWithDefaults; + options: FeedbackConfigurationWithDefaults & {referrer?: string}; attachTo?: Node; } /** - * + * Creates a new widget. Returns public methods that control widget behavior. */ export function createWidget({ shadow, options, attachTo }: CreateWidgetParams): Widget { let actor: ActorComponent | undefined; @@ -64,7 +64,7 @@ export function createWidget({ shadow, options, attachTo }: CreateWidgetParams): return; } - const result = await handleFeedbackSubmit(dialog, feedback); + const result = await handleFeedbackSubmit(dialog, feedback, {referrer: options.referrer}); // Error submitting feedback if (!result) { From 171586dcec47e36c6df9ad0104b2d6631245f70c Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 25 Oct 2023 12:08:16 -0400 Subject: [PATCH 13/15] document --> WINDOW.document --- packages/feedback/src/integration.ts | 16 +++++++--------- packages/feedback/src/widget/Icon.ts | 4 ++-- packages/feedback/src/widget/SuccessIcon.ts | 4 ++-- packages/feedback/src/widget/createShadowHost.ts | 8 +++++--- .../feedback/src/widget/util/createElement.ts | 13 +++++++------ 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/feedback/src/integration.ts b/packages/feedback/src/integration.ts index bbb491a4daac..795b0d140961 100644 --- a/packages/feedback/src/integration.ts +++ b/packages/feedback/src/integration.ts @@ -1,3 +1,4 @@ +import { WINDOW } from '@sentry/browser'; import type { Integration } from '@sentry/types'; import { isBrowser } from '@sentry/utils'; import { logger } from '@sentry/utils'; @@ -46,6 +47,8 @@ function isBrowser(): boolean { type FeedbackConfiguration = Partial; >>>>>>> 5fa9a4abb (ref: extract widget creation to function, allow handling of multiple widgets) +const doc = WINDOW.document; + /** * Feedback integration. When added as an integration to the SDK, it will * inject a button in the bottom-right corner of the window that opens a @@ -184,8 +187,7 @@ export class Feedback implements Integration { if (this._host) { this.remove(); } - // eslint-disable-next-line no-restricted-globals - const existingFeedback = document.querySelector(`#${this.options.id}`); + const existingFeedback = doc.querySelector(`#${this.options.id}`); if (existingFeedback) { existingFeedback.remove(); } @@ -200,7 +202,6 @@ export class Feedback implements Integration { this._widget = this._createWidget(this.options); } catch (err) { - // TODO: error handling logger.error(err); } } @@ -214,8 +215,7 @@ export class Feedback implements Integration { return this._ensureShadowHost(options, ([shadow]) => { const targetEl = - // eslint-disable-next-line no-restricted-globals - typeof el === 'string' ? document.querySelector(el) : typeof el.addEventListener === 'function' ? el : null; + typeof el === 'string' ? doc.querySelector(el) : typeof el.addEventListener === 'function' ? el : null; if (!targetEl) { logger.error('[Feedback] Unable to attach to target element'); @@ -294,8 +294,7 @@ export class Feedback implements Integration { const widget = createWidget({ shadow, options }); if (!this._hasInsertedActorStyles && widget.actor) { - // eslint-disable-next-line no-restricted-globals - shadow.appendChild(createActorStyles(document)); + shadow.appendChild(createActorStyles(doc)); this._hasInsertedActorStyles = true; } @@ -333,8 +332,7 @@ export class Feedback implements Integration { const result = cb([this._shadow, this._host]); if (needsAppendHost) { - // eslint-disable-next-line no-restricted-globals - document.body.appendChild(this._host); + doc.body.appendChild(this._host); } return result; diff --git a/packages/feedback/src/widget/Icon.ts b/packages/feedback/src/widget/Icon.ts index 5a305cdb3d64..5b50715d69df 100644 --- a/packages/feedback/src/widget/Icon.ts +++ b/packages/feedback/src/widget/Icon.ts @@ -1,3 +1,4 @@ +import { WINDOW } from '@sentry/browser'; import { setAttributesNS } from '../util/setAttributesNS'; const SIZE = 20; @@ -12,8 +13,7 @@ interface IconReturn { */ export function Icon(): IconReturn { const cENS = (tagName: K): SVGElementTagNameMap[K] => - // eslint-disable-next-line no-restricted-globals - document.createElementNS(XMLNS, tagName); + WINDOW.document.createElementNS(XMLNS, tagName); const svg = setAttributesNS(cENS('svg'), { class: 'feedback-icon', width: `${SIZE}`, diff --git a/packages/feedback/src/widget/SuccessIcon.ts b/packages/feedback/src/widget/SuccessIcon.ts index a21307ccfd31..3e2cda5e135b 100644 --- a/packages/feedback/src/widget/SuccessIcon.ts +++ b/packages/feedback/src/widget/SuccessIcon.ts @@ -1,3 +1,4 @@ +import { WINDOW } from '@sentry/browser'; import { setAttributesNS } from '../util/setAttributesNS'; const WIDTH = 16; @@ -13,8 +14,7 @@ interface IconReturn { */ export function SuccessIcon(): IconReturn { const cENS = (tagName: K): SVGElementTagNameMap[K] => - // eslint-disable-next-line no-restricted-globals - document.createElementNS(XMLNS, tagName); + WINDOW.document.createElementNS(XMLNS, tagName); const svg = setAttributesNS(cENS('svg'), { class: 'success-icon', width: `${WIDTH}`, diff --git a/packages/feedback/src/widget/createShadowHost.ts b/packages/feedback/src/widget/createShadowHost.ts index d75dc9020db5..66428299cee9 100644 --- a/packages/feedback/src/widget/createShadowHost.ts +++ b/packages/feedback/src/widget/createShadowHost.ts @@ -1,3 +1,4 @@ +import { WINDOW } from '@sentry/browser'; import { logger } from '@sentry/utils'; import type { FeedbackConfigurationWithDefaults } from '../types'; @@ -7,12 +8,13 @@ import { createMainStyles } from './Main.css'; interface CreateShadowHostParams { options: FeedbackConfigurationWithDefaults; } + /** - * + * Creates shadow host */ export function createShadowHost({ options }: CreateShadowHostParams): [shadow: ShadowRoot, host: HTMLDivElement] { - // eslint-disable-next-line no-restricted-globals - const doc = document; + const doc = WINDOW.document; + if (!doc.head.attachShadow) { // Shadow DOM not supported logger.warn('[Feedback] Browser does not support shadow DOM API'); diff --git a/packages/feedback/src/widget/util/createElement.ts b/packages/feedback/src/widget/util/createElement.ts index 4d9849a19f54..bf5f81868d68 100644 --- a/packages/feedback/src/widget/util/createElement.ts +++ b/packages/feedback/src/widget/util/createElement.ts @@ -1,3 +1,5 @@ +import { WINDOW } from '@sentry/browser'; + /** * Helper function to create an element. Could be used as a JSX factory * (i.e. React-like syntax). @@ -7,8 +9,8 @@ export function createElement( attributes: { [key: string]: string | boolean | EventListenerOrEventListenerObject } | null, ...children: any ): HTMLElementTagNameMap[K] { - // eslint-disable-next-line no-restricted-globals - const element = document.createElement(tagName); + const doc = WINDOW.document; + const element = doc.createElement(tagName); if (attributes) { Object.entries(attributes).forEach(([attribute, attributeValue]) => { @@ -32,6 +34,7 @@ export function createElement( } function appendChild(parent: Node, child: any): void { + const doc = WINDOW.document; if (typeof child === 'undefined' || child === null) { return; } @@ -43,12 +46,10 @@ function appendChild(parent: Node, child: any): void { } else if (child === false) { // do nothing if child evaluated to false } else if (typeof child === 'string') { - // eslint-disable-next-line no-restricted-globals - parent.appendChild(document.createTextNode(child)); + parent.appendChild(doc.createTextNode(child)); } else if (child instanceof Node) { parent.appendChild(child); } else { - // eslint-disable-next-line no-restricted-globals - parent.appendChild(document.createTextNode(String(child))); + parent.appendChild(doc.createTextNode(String(child))); } } From 2815e9b620be81c8b3aa405f3e70be0b53a66002 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 25 Oct 2023 12:08:25 -0400 Subject: [PATCH 14/15] bump versions --- packages/feedback/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 3164094b2838..e2b5041acbb2 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -23,10 +23,10 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "7.75.0", - "@sentry/core": "7.75.0", - "@sentry/types": "7.75.0", - "@sentry/utils": "7.75.0" + "@sentry/browser": "7.75.1", + "@sentry/core": "7.75.1", + "@sentry/types": "7.75.1", + "@sentry/utils": "7.75.1" }, "scripts": { "build": "run-p build:transpile build:types build:bundle", From 267ba37051240399c6f7cfdc330a6b069fa7cc70 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 25 Oct 2023 12:16:31 -0400 Subject: [PATCH 15/15] missed this conflict --- packages/feedback/src/integration.ts | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/packages/feedback/src/integration.ts b/packages/feedback/src/integration.ts index 795b0d140961..9075f45d377b 100644 --- a/packages/feedback/src/integration.ts +++ b/packages/feedback/src/integration.ts @@ -1,7 +1,6 @@ import { WINDOW } from '@sentry/browser'; import type { Integration } from '@sentry/types'; -import { isBrowser } from '@sentry/utils'; -import { logger } from '@sentry/utils'; +import { isBrowser, logger } from '@sentry/utils'; import { ACTOR_LABEL, @@ -22,32 +21,9 @@ import { createActorStyles } from './widget/Actor.css'; import { createShadowHost } from './widget/createShadowHost'; import { createWidget } from './widget/createWidget'; -<<<<<<< HEAD -interface FeedbackConfiguration extends Partial> { - theme?: { - dark?: Partial; - light?: Partial; - }; -} -======= -type ElectronProcess = { type?: string }; - -// Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them -function isElectronNodeRenderer(): boolean { - return typeof process !== 'undefined' && (process as ElectronProcess).type === 'renderer'; -} -/** - * Returns true if we are in the browser. - */ -function isBrowser(): boolean { - // eslint-disable-next-line no-restricted-globals - return typeof window !== 'undefined' && (!isNodeEnv() || isElectronNodeRenderer()); -} +const doc = WINDOW.document; type FeedbackConfiguration = Partial; ->>>>>>> 5fa9a4abb (ref: extract widget creation to function, allow handling of multiple widgets) - -const doc = WINDOW.document; /** * Feedback integration. When added as an integration to the SDK, it will