@sentry-internal/feedback
Advanced tools
Comparing version 0.0.1-alpha.4 to 0.0.1-alpha.5
@@ -5,2 +5,3 @@ Object.defineProperty(exports, '__esModule', { value: true }); | ||
const utils = require('@sentry/utils'); | ||
const browser = require('@sentry/browser'); | ||
@@ -54,2 +55,3 @@ /** | ||
feedback: { message, email, name, replay_id, url }, | ||
referrer, | ||
}) { | ||
@@ -79,2 +81,5 @@ const hub = core.getCurrentHub(); | ||
}, | ||
tags: { | ||
referrer, | ||
}, | ||
// type: 'feedback_event', | ||
@@ -499,26 +504,26 @@ }; | ||
/** | ||
* | ||
* Creates shadow host | ||
*/ | ||
function createShadowHost({ options }) { | ||
// eslint-disable-next-line no-restricted-globals | ||
const doc = document; | ||
if (!doc.head.attachShadow) { | ||
// Shadow DOM not supported | ||
utils.logger.warn('[Feedback] Browser does not support shadow DOM API'); | ||
throw new Error('Browser does not support shadow DOM API.'); | ||
} | ||
try { | ||
const doc = browser.WINDOW.document; | ||
// Create the host | ||
const host = doc.createElement('div'); | ||
host.id = options.id; | ||
// Create the host | ||
const host = doc.createElement('div'); | ||
host.id = options.id; | ||
// Create the shadow root | ||
const shadow = host.attachShadow({ mode: 'open' }); | ||
// 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)); | ||
shadow.appendChild( | ||
createMainStyles(doc, options.colorScheme, { dark: options.themeDark, light: options.themeLight }), | ||
); | ||
shadow.appendChild(createDialogStyles(doc)); | ||
return [shadow, host]; | ||
return { shadow, host }; | ||
} catch (e) { | ||
// Shadow DOM probably not supported | ||
utils.logger.warn('[Feedback] Browser does not support shadow DOM API'); | ||
throw new Error('Browser does not support shadow DOM API.'); | ||
} | ||
} | ||
@@ -531,3 +536,3 @@ | ||
{ name, email, message, url = utils.getLocationHref() }, | ||
{ includeReplay = true } = {}, | ||
{ referrer, includeReplay = true } = {}, | ||
) { | ||
@@ -550,2 +555,3 @@ const hub = core.getCurrentHub(); | ||
}, | ||
referrer, | ||
}); | ||
@@ -555,3 +561,3 @@ } | ||
/** | ||
* | ||
* Calls `sendFeedback` to send feedback, handles UI behavior of dialog. | ||
*/ | ||
@@ -561,2 +567,3 @@ async function handleFeedbackSubmit( | ||
feedback, | ||
options, | ||
) { | ||
@@ -579,3 +586,3 @@ if (!dialog) { | ||
dialog.setSubmitDisabled(); | ||
const resp = await sendFeedback(feedback); | ||
const resp = await sendFeedback(feedback, options); | ||
@@ -614,6 +621,5 @@ if (!resp) { | ||
function Icon() { | ||
const cENS = (tagName) => | ||
// eslint-disable-next-line no-restricted-globals | ||
document.createElementNS(XMLNS$1, tagName); | ||
const svg = setAttributesNS(cENS('svg'), { | ||
const createElementNS = (tagName) => | ||
browser.WINDOW.document.createElementNS(XMLNS$1, tagName); | ||
const svg = setAttributesNS(createElementNS('svg'), { | ||
class: 'feedback-icon', | ||
@@ -626,7 +632,7 @@ width: `${SIZE}`, | ||
const g = setAttributesNS(cENS('g'), { | ||
const g = setAttributesNS(createElementNS('g'), { | ||
clipPath: 'url(#clip0_57_80)', | ||
}); | ||
const path = setAttributesNS(cENS('path'), { | ||
const path = setAttributesNS(createElementNS('path'), { | ||
['fill-rule']: 'evenodd', | ||
@@ -638,8 +644,8 @@ ['clip-rule']: 'evenodd', | ||
const speakerDefs = cENS('defs'); | ||
const speakerClipPathDef = setAttributesNS(cENS('clipPath'), { | ||
const speakerDefs = createElementNS('defs'); | ||
const speakerClipPathDef = setAttributesNS(createElementNS('clipPath'), { | ||
id: 'clip0_57_80', | ||
}); | ||
const speakerRect = setAttributesNS(cENS('rect'), { | ||
const speakerRect = setAttributesNS(createElementNS('rect'), { | ||
width: `${SIZE}`, | ||
@@ -656,3 +662,3 @@ height: `${SIZE}`, | ||
return { | ||
$el: svg, | ||
el: svg, | ||
}; | ||
@@ -670,4 +676,4 @@ } | ||
) { | ||
// eslint-disable-next-line no-restricted-globals | ||
const element = document.createElement(tagName); | ||
const doc = browser.WINDOW.document; | ||
const element = doc.createElement(tagName); | ||
@@ -696,2 +702,3 @@ if (attributes) { | ||
function appendChild(parent, child) { | ||
const doc = browser.WINDOW.document; | ||
if (typeof child === 'undefined' || child === null) { | ||
@@ -706,9 +713,7 @@ return; | ||
} else if (child === 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))); | ||
} | ||
@@ -725,3 +730,3 @@ } | ||
const $el = createElement( | ||
const el = createElement( | ||
'button', | ||
@@ -733,3 +738,3 @@ { | ||
}, | ||
Icon().$el, | ||
Icon().el, | ||
createElement( | ||
@@ -744,11 +749,11 @@ 'span', | ||
$el.addEventListener('click', _handleClick); | ||
el.addEventListener('click', _handleClick); | ||
return { | ||
$el, | ||
el, | ||
show: () => { | ||
$el.classList.remove('widget__actor--hidden'); | ||
el.classList.remove('widget__actor--hidden'); | ||
}, | ||
hide: () => { | ||
$el.classList.add('widget__actor--hidden'); | ||
el.classList.add('widget__actor--hidden'); | ||
}, | ||
@@ -762,3 +767,3 @@ }; | ||
function SubmitButton({ label }) { | ||
const $el = createElement( | ||
const el = createElement( | ||
'button', | ||
@@ -775,11 +780,11 @@ { | ||
return { | ||
$el, | ||
el, | ||
setDisabled: () => { | ||
$el.disabled = true; | ||
$el.ariaDisabled = 'disabled'; | ||
el.disabled = true; | ||
el.ariaDisabled = 'disabled'; | ||
}, | ||
setEnabled: () => { | ||
$el.disabled = false; | ||
$el.ariaDisabled = 'false'; | ||
$el.removeAttribute('ariaDisabled'); | ||
el.disabled = false; | ||
el.ariaDisabled = 'false'; | ||
el.removeAttribute('ariaDisabled'); | ||
}, | ||
@@ -822,3 +827,3 @@ }; | ||
const { | ||
$el: $submit, | ||
el: submitEl, | ||
setDisabled: setSubmitDisabled, | ||
@@ -853,3 +858,3 @@ setEnabled: setSubmitEnabled, | ||
const $error = createElement('div', { | ||
const errorEl = createElement('div', { | ||
className: 'form__error-container form__error-container--hidden', | ||
@@ -860,14 +865,14 @@ ariaHidden: 'true', | ||
function showError(message) { | ||
$error.textContent = message; | ||
$error.classList.remove('form__error-container--hidden'); | ||
$error.setAttribute('ariaHidden', 'false'); | ||
errorEl.textContent = message; | ||
errorEl.classList.remove('form__error-container--hidden'); | ||
errorEl.setAttribute('ariaHidden', 'false'); | ||
} | ||
function hideError() { | ||
$error.textContent = ''; | ||
$error.classList.add('form__error-container--hidden'); | ||
$error.setAttribute('ariaHidden', 'true'); | ||
errorEl.textContent = ''; | ||
errorEl.classList.add('form__error-container--hidden'); | ||
errorEl.setAttribute('ariaHidden', 'true'); | ||
} | ||
const $name = createElement('input', { | ||
const nameEl = createElement('input', { | ||
id: 'name', | ||
@@ -882,3 +887,3 @@ type: showName ? 'text' : 'hidden', | ||
const $email = createElement('input', { | ||
const emailEl = createElement('input', { | ||
id: 'email', | ||
@@ -893,3 +898,3 @@ type: showEmail ? 'text' : 'hidden', | ||
const $message = createElement('textarea', { | ||
const messageEl = createElement('textarea', { | ||
id: 'message', | ||
@@ -914,3 +919,3 @@ autoFocus: 'true', | ||
const $cancel = createElement( | ||
const cancelEl = createElement( | ||
'button', | ||
@@ -927,3 +932,3 @@ { | ||
const $form = createElement( | ||
const formEl = createElement( | ||
'form', | ||
@@ -935,3 +940,3 @@ { | ||
[ | ||
$error, | ||
errorEl, | ||
@@ -946,5 +951,5 @@ !isAnonymous && | ||
}, | ||
[nameLabel, $name], | ||
[nameLabel, nameEl], | ||
), | ||
!isAnonymous && !showName && $name, | ||
!isAnonymous && !showName && nameEl, | ||
@@ -959,5 +964,5 @@ !isAnonymous && | ||
}, | ||
[emailLabel, $email], | ||
[emailLabel, emailEl], | ||
), | ||
!isAnonymous && !showEmail && $email, | ||
!isAnonymous && !showEmail && emailEl, | ||
@@ -970,3 +975,3 @@ createElement( | ||
}, | ||
[messageLabel, $message], | ||
[messageLabel, messageEl], | ||
), | ||
@@ -979,3 +984,3 @@ | ||
}, | ||
[$submit, $cancel], | ||
[submitEl, cancelEl], | ||
), | ||
@@ -986,3 +991,3 @@ ], | ||
return { | ||
$el: $form, | ||
el: formEl, | ||
setSubmitDisabled, | ||
@@ -1006,3 +1011,3 @@ setSubmitEnabled, | ||
}) { | ||
let $el = null; | ||
let el = null; | ||
@@ -1026,4 +1031,4 @@ /** | ||
function close() { | ||
if ($el) { | ||
$el.open = false; | ||
if (el) { | ||
el.open = false; | ||
} | ||
@@ -1036,4 +1041,4 @@ } | ||
function open() { | ||
if ($el) { | ||
$el.open = true; | ||
if (el) { | ||
el.open = true; | ||
} | ||
@@ -1046,7 +1051,7 @@ } | ||
function checkIsOpen() { | ||
return ($el && $el.open === true) || false; | ||
return (el && el.open === true) || false; | ||
} | ||
const { | ||
$el: $form, | ||
el: formEl, | ||
setSubmitEnabled, | ||
@@ -1064,3 +1069,3 @@ setSubmitDisabled, | ||
$el = createElement( | ||
el = createElement( | ||
'dialog', | ||
@@ -1082,3 +1087,3 @@ { | ||
createElement('h2', { className: 'dialog__header' }, options.formTitle), | ||
$form, | ||
formEl, | ||
), | ||
@@ -1088,3 +1093,3 @@ ); | ||
return { | ||
$el, | ||
el, | ||
showError, | ||
@@ -1108,6 +1113,5 @@ hideError, | ||
function SuccessIcon() { | ||
const cENS = (tagName) => | ||
// eslint-disable-next-line no-restricted-globals | ||
document.createElementNS(XMLNS, tagName); | ||
const svg = setAttributesNS(cENS('svg'), { | ||
const createElementNS = (tagName) => | ||
browser.WINDOW.document.createElementNS(XMLNS, tagName); | ||
const svg = setAttributesNS(createElementNS('svg'), { | ||
class: 'success-icon', | ||
@@ -1120,7 +1124,7 @@ width: `${WIDTH}`, | ||
const g = setAttributesNS(cENS('g'), { | ||
const g = setAttributesNS(createElementNS('g'), { | ||
clipPath: 'url(#clip0_57_156)', | ||
}); | ||
const path2 = setAttributesNS(cENS('path'), { | ||
const path2 = setAttributesNS(createElementNS('path'), { | ||
['fill-rule']: 'evenodd', | ||
@@ -1130,3 +1134,3 @@ ['clip-rule']: 'evenodd', | ||
}); | ||
const path = setAttributesNS(cENS('path'), { | ||
const path = setAttributesNS(createElementNS('path'), { | ||
d: 'M6.68775 12.4297C6.78586 12.4745 6.89218 12.4984 7 12.5C7.11275 12.4955 7.22315 12.4664 7.32337 12.4145C7.4236 12.3627 7.51121 12.2894 7.58 12.2L12 5.63999C12.0848 5.47724 12.1071 5.28902 12.0625 5.11098C12.0178 4.93294 11.9095 4.77744 11.7579 4.67392C11.6064 4.57041 11.4221 4.52608 11.24 4.54931C11.0579 4.57254 10.8907 4.66173 10.77 4.79999L6.88 10.57L5.13 8.56999C5.06508 8.49566 4.98613 8.43488 4.89768 8.39111C4.80922 8.34735 4.713 8.32148 4.61453 8.31498C4.51605 8.30847 4.41727 8.32147 4.32382 8.35322C4.23038 8.38497 4.14413 8.43484 4.07 8.49999C3.92511 8.63217 3.83692 8.81523 3.82387 9.01092C3.81083 9.2066 3.87393 9.39976 4 9.54999L6.43 12.24C6.50187 12.3204 6.58964 12.385 6.68775 12.4297Z', | ||
@@ -1137,8 +1141,8 @@ }); | ||
const speakerDefs = cENS('defs'); | ||
const speakerClipPathDef = setAttributesNS(cENS('clipPath'), { | ||
const speakerDefs = createElementNS('defs'); | ||
const speakerClipPathDef = setAttributesNS(createElementNS('clipPath'), { | ||
id: 'clip0_57_156', | ||
}); | ||
const speakerRect = setAttributesNS(cENS('rect'), { | ||
const speakerRect = setAttributesNS(createElementNS('rect'), { | ||
width: `${WIDTH}`, | ||
@@ -1156,3 +1160,3 @@ height: `${WIDTH}`, | ||
return { | ||
$el: svg, | ||
el: svg, | ||
}; | ||
@@ -1166,11 +1170,11 @@ } | ||
function remove() { | ||
if (!$el) { | ||
if (!el) { | ||
return; | ||
} | ||
$el.remove(); | ||
el.remove(); | ||
onRemove && onRemove(); | ||
} | ||
const $el = createElement( | ||
const el = createElement( | ||
'div', | ||
@@ -1181,3 +1185,3 @@ { | ||
}, | ||
SuccessIcon().$el, | ||
SuccessIcon().el, | ||
message, | ||
@@ -1187,3 +1191,3 @@ ); | ||
return { | ||
$el, | ||
el, | ||
remove, | ||
@@ -1194,3 +1198,3 @@ }; | ||
/** | ||
* | ||
* Creates a new widget. Returns public methods that control widget behavior. | ||
*/ | ||
@@ -1221,3 +1225,3 @@ function createWidget({ shadow, options, attachTo }) { | ||
shadow.appendChild(success.$el); | ||
shadow.appendChild(success.el); | ||
@@ -1244,3 +1248,3 @@ const timeoutId = setTimeout(() => { | ||
const result = await handleFeedbackSubmit(dialog, feedback); | ||
const result = await handleFeedbackSubmit(dialog, feedback, { referrer: options.referrer }); | ||
@@ -1283,3 +1287,3 @@ // Error submitting feedback | ||
function removeActor() { | ||
actor && actor.$el.remove(); | ||
actor && actor.el.remove(); | ||
} | ||
@@ -1320,3 +1324,3 @@ | ||
shadow.appendChild(dialog.$el); | ||
shadow.appendChild(dialog.el); | ||
@@ -1355,3 +1359,3 @@ // Hides the default actor whenever dialog is opened | ||
hideDialog(); | ||
dialog.$el.remove(); | ||
dialog.el.remove(); | ||
dialog = undefined; | ||
@@ -1380,3 +1384,3 @@ } | ||
actor = Actor({ options, onClick: handleActorClick }); | ||
shadow.appendChild(actor.$el); | ||
shadow.appendChild(actor.el); | ||
} else { | ||
@@ -1400,13 +1404,3 @@ attachTo.addEventListener('click', handleActorClick); | ||
// Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them | ||
function isElectronNodeRenderer() { | ||
return typeof process !== 'undefined' && (process ).type === 'renderer'; | ||
} | ||
/** | ||
* Returns true if we are in the browser. | ||
*/ | ||
function isBrowser() { | ||
// eslint-disable-next-line no-restricted-globals | ||
return typeof window !== 'undefined' && (!utils.isNodeEnv() || isElectronNodeRenderer()); | ||
} | ||
const doc = browser.WINDOW.document; | ||
@@ -1454,3 +1448,2 @@ /** | ||
id = 'sentry-feedback', | ||
// attachTo = null, | ||
autoInject = true, | ||
@@ -1501,3 +1494,2 @@ showEmail = true, | ||
id, | ||
// attachTo, | ||
autoInject, | ||
@@ -1539,3 +1531,3 @@ isAnonymous, | ||
setupOnce() { | ||
if (!isBrowser()) { | ||
if (!utils.isBrowser()) { | ||
return; | ||
@@ -1549,4 +1541,3 @@ } | ||
} | ||
// eslint-disable-next-line no-restricted-globals | ||
const existingFeedback = document.querySelector(`#${this.options.id}`); | ||
const existingFeedback = doc.querySelector(`#${this.options.id}`); | ||
if (existingFeedback) { | ||
@@ -1566,3 +1557,2 @@ existingFeedback.remove(); | ||
} catch (err) { | ||
// TODO: error handling | ||
utils.logger.error(err); | ||
@@ -1579,6 +1569,5 @@ } | ||
return this._ensureShadowHost(options, ([shadow]) => { | ||
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; | ||
@@ -1630,2 +1619,3 @@ if (!targetEl) { | ||
} | ||
return false; | ||
@@ -1659,8 +1649,7 @@ } | ||
_createWidget(options) { | ||
return this._ensureShadowHost(options, ([shadow]) => { | ||
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)); | ||
shadow.appendChild(createActorStyles(doc)); | ||
this._hasInsertedActorStyles = true; | ||
@@ -1684,4 +1673,4 @@ } | ||
// Don't create if it already exists | ||
if (!this._shadow && !this._host) { | ||
const [shadow, host] = createShadowHost({ options }); | ||
if (!this._shadow || !this._host) { | ||
const { shadow, host } = createShadowHost({ options }); | ||
this._shadow = shadow; | ||
@@ -1692,16 +1681,9 @@ this._host = host; | ||
if (!this._shadow || !this._host) { | ||
utils.logger.warn('[Feedback] Unable to create host element and/or shadow DOM'); | ||
// This shouldn't happen | ||
return null; | ||
} | ||
// set data attribute on host for different themes | ||
this._host.dataset.sentryFeedbackColorscheme = options.colorScheme; | ||
const result = cb([this._shadow, this._host]); | ||
const result = cb({ shadow: this._shadow, host: this._host }); | ||
if (needsAppendHost) { | ||
// eslint-disable-next-line no-restricted-globals | ||
document.body.appendChild(this._host); | ||
doc.body.appendChild(this._host); | ||
} | ||
@@ -1708,0 +1690,0 @@ |
import { prepareEvent, getCurrentHub } from '@sentry/core'; | ||
import { dsnToString, logger, getLocationHref, isNodeEnv } from '@sentry/utils'; | ||
import { dsnToString, logger, getLocationHref, isBrowser } from '@sentry/utils'; | ||
import { WINDOW } from '@sentry/browser'; | ||
@@ -51,2 +52,3 @@ /** | ||
feedback: { message, email, name, replay_id, url }, | ||
referrer, | ||
}) { | ||
@@ -76,2 +78,5 @@ const hub = getCurrentHub(); | ||
}, | ||
tags: { | ||
referrer, | ||
}, | ||
// type: 'feedback_event', | ||
@@ -496,26 +501,26 @@ }; | ||
/** | ||
* | ||
* Creates shadow host | ||
*/ | ||
function createShadowHost({ options }) { | ||
// 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.'); | ||
} | ||
try { | ||
const doc = WINDOW.document; | ||
// Create the host | ||
const host = doc.createElement('div'); | ||
host.id = options.id; | ||
// Create the host | ||
const host = doc.createElement('div'); | ||
host.id = options.id; | ||
// Create the shadow root | ||
const shadow = host.attachShadow({ mode: 'open' }); | ||
// 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)); | ||
shadow.appendChild( | ||
createMainStyles(doc, options.colorScheme, { dark: options.themeDark, light: options.themeLight }), | ||
); | ||
shadow.appendChild(createDialogStyles(doc)); | ||
return [shadow, host]; | ||
return { shadow, host }; | ||
} catch (e) { | ||
// Shadow DOM probably not supported | ||
logger.warn('[Feedback] Browser does not support shadow DOM API'); | ||
throw new Error('Browser does not support shadow DOM API.'); | ||
} | ||
} | ||
@@ -528,3 +533,3 @@ | ||
{ name, email, message, url = getLocationHref() }, | ||
{ includeReplay = true } = {}, | ||
{ referrer, includeReplay = true } = {}, | ||
) { | ||
@@ -547,2 +552,3 @@ const hub = getCurrentHub(); | ||
}, | ||
referrer, | ||
}); | ||
@@ -552,3 +558,3 @@ } | ||
/** | ||
* | ||
* Calls `sendFeedback` to send feedback, handles UI behavior of dialog. | ||
*/ | ||
@@ -558,2 +564,3 @@ async function handleFeedbackSubmit( | ||
feedback, | ||
options, | ||
) { | ||
@@ -576,3 +583,3 @@ if (!dialog) { | ||
dialog.setSubmitDisabled(); | ||
const resp = await sendFeedback(feedback); | ||
const resp = await sendFeedback(feedback, options); | ||
@@ -611,6 +618,5 @@ if (!resp) { | ||
function Icon() { | ||
const cENS = (tagName) => | ||
// eslint-disable-next-line no-restricted-globals | ||
document.createElementNS(XMLNS$1, tagName); | ||
const svg = setAttributesNS(cENS('svg'), { | ||
const createElementNS = (tagName) => | ||
WINDOW.document.createElementNS(XMLNS$1, tagName); | ||
const svg = setAttributesNS(createElementNS('svg'), { | ||
class: 'feedback-icon', | ||
@@ -623,7 +629,7 @@ width: `${SIZE}`, | ||
const g = setAttributesNS(cENS('g'), { | ||
const g = setAttributesNS(createElementNS('g'), { | ||
clipPath: 'url(#clip0_57_80)', | ||
}); | ||
const path = setAttributesNS(cENS('path'), { | ||
const path = setAttributesNS(createElementNS('path'), { | ||
['fill-rule']: 'evenodd', | ||
@@ -635,8 +641,8 @@ ['clip-rule']: 'evenodd', | ||
const speakerDefs = cENS('defs'); | ||
const speakerClipPathDef = setAttributesNS(cENS('clipPath'), { | ||
const speakerDefs = createElementNS('defs'); | ||
const speakerClipPathDef = setAttributesNS(createElementNS('clipPath'), { | ||
id: 'clip0_57_80', | ||
}); | ||
const speakerRect = setAttributesNS(cENS('rect'), { | ||
const speakerRect = setAttributesNS(createElementNS('rect'), { | ||
width: `${SIZE}`, | ||
@@ -653,3 +659,3 @@ height: `${SIZE}`, | ||
return { | ||
$el: svg, | ||
el: svg, | ||
}; | ||
@@ -667,4 +673,4 @@ } | ||
) { | ||
// eslint-disable-next-line no-restricted-globals | ||
const element = document.createElement(tagName); | ||
const doc = WINDOW.document; | ||
const element = doc.createElement(tagName); | ||
@@ -693,2 +699,3 @@ if (attributes) { | ||
function appendChild(parent, child) { | ||
const doc = WINDOW.document; | ||
if (typeof child === 'undefined' || child === null) { | ||
@@ -703,9 +710,7 @@ return; | ||
} else if (child === 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))); | ||
} | ||
@@ -722,3 +727,3 @@ } | ||
const $el = createElement( | ||
const el = createElement( | ||
'button', | ||
@@ -730,3 +735,3 @@ { | ||
}, | ||
Icon().$el, | ||
Icon().el, | ||
createElement( | ||
@@ -741,11 +746,11 @@ 'span', | ||
$el.addEventListener('click', _handleClick); | ||
el.addEventListener('click', _handleClick); | ||
return { | ||
$el, | ||
el, | ||
show: () => { | ||
$el.classList.remove('widget__actor--hidden'); | ||
el.classList.remove('widget__actor--hidden'); | ||
}, | ||
hide: () => { | ||
$el.classList.add('widget__actor--hidden'); | ||
el.classList.add('widget__actor--hidden'); | ||
}, | ||
@@ -759,3 +764,3 @@ }; | ||
function SubmitButton({ label }) { | ||
const $el = createElement( | ||
const el = createElement( | ||
'button', | ||
@@ -772,11 +777,11 @@ { | ||
return { | ||
$el, | ||
el, | ||
setDisabled: () => { | ||
$el.disabled = true; | ||
$el.ariaDisabled = 'disabled'; | ||
el.disabled = true; | ||
el.ariaDisabled = 'disabled'; | ||
}, | ||
setEnabled: () => { | ||
$el.disabled = false; | ||
$el.ariaDisabled = 'false'; | ||
$el.removeAttribute('ariaDisabled'); | ||
el.disabled = false; | ||
el.ariaDisabled = 'false'; | ||
el.removeAttribute('ariaDisabled'); | ||
}, | ||
@@ -819,3 +824,3 @@ }; | ||
const { | ||
$el: $submit, | ||
el: submitEl, | ||
setDisabled: setSubmitDisabled, | ||
@@ -850,3 +855,3 @@ setEnabled: setSubmitEnabled, | ||
const $error = createElement('div', { | ||
const errorEl = createElement('div', { | ||
className: 'form__error-container form__error-container--hidden', | ||
@@ -857,14 +862,14 @@ ariaHidden: 'true', | ||
function showError(message) { | ||
$error.textContent = message; | ||
$error.classList.remove('form__error-container--hidden'); | ||
$error.setAttribute('ariaHidden', 'false'); | ||
errorEl.textContent = message; | ||
errorEl.classList.remove('form__error-container--hidden'); | ||
errorEl.setAttribute('ariaHidden', 'false'); | ||
} | ||
function hideError() { | ||
$error.textContent = ''; | ||
$error.classList.add('form__error-container--hidden'); | ||
$error.setAttribute('ariaHidden', 'true'); | ||
errorEl.textContent = ''; | ||
errorEl.classList.add('form__error-container--hidden'); | ||
errorEl.setAttribute('ariaHidden', 'true'); | ||
} | ||
const $name = createElement('input', { | ||
const nameEl = createElement('input', { | ||
id: 'name', | ||
@@ -879,3 +884,3 @@ type: showName ? 'text' : 'hidden', | ||
const $email = createElement('input', { | ||
const emailEl = createElement('input', { | ||
id: 'email', | ||
@@ -890,3 +895,3 @@ type: showEmail ? 'text' : 'hidden', | ||
const $message = createElement('textarea', { | ||
const messageEl = createElement('textarea', { | ||
id: 'message', | ||
@@ -911,3 +916,3 @@ autoFocus: 'true', | ||
const $cancel = createElement( | ||
const cancelEl = createElement( | ||
'button', | ||
@@ -924,3 +929,3 @@ { | ||
const $form = createElement( | ||
const formEl = createElement( | ||
'form', | ||
@@ -932,3 +937,3 @@ { | ||
[ | ||
$error, | ||
errorEl, | ||
@@ -943,5 +948,5 @@ !isAnonymous && | ||
}, | ||
[nameLabel, $name], | ||
[nameLabel, nameEl], | ||
), | ||
!isAnonymous && !showName && $name, | ||
!isAnonymous && !showName && nameEl, | ||
@@ -956,5 +961,5 @@ !isAnonymous && | ||
}, | ||
[emailLabel, $email], | ||
[emailLabel, emailEl], | ||
), | ||
!isAnonymous && !showEmail && $email, | ||
!isAnonymous && !showEmail && emailEl, | ||
@@ -967,3 +972,3 @@ createElement( | ||
}, | ||
[messageLabel, $message], | ||
[messageLabel, messageEl], | ||
), | ||
@@ -976,3 +981,3 @@ | ||
}, | ||
[$submit, $cancel], | ||
[submitEl, cancelEl], | ||
), | ||
@@ -983,3 +988,3 @@ ], | ||
return { | ||
$el: $form, | ||
el: formEl, | ||
setSubmitDisabled, | ||
@@ -1003,3 +1008,3 @@ setSubmitEnabled, | ||
}) { | ||
let $el = null; | ||
let el = null; | ||
@@ -1023,4 +1028,4 @@ /** | ||
function close() { | ||
if ($el) { | ||
$el.open = false; | ||
if (el) { | ||
el.open = false; | ||
} | ||
@@ -1033,4 +1038,4 @@ } | ||
function open() { | ||
if ($el) { | ||
$el.open = true; | ||
if (el) { | ||
el.open = true; | ||
} | ||
@@ -1043,7 +1048,7 @@ } | ||
function checkIsOpen() { | ||
return ($el && $el.open === true) || false; | ||
return (el && el.open === true) || false; | ||
} | ||
const { | ||
$el: $form, | ||
el: formEl, | ||
setSubmitEnabled, | ||
@@ -1061,3 +1066,3 @@ setSubmitDisabled, | ||
$el = createElement( | ||
el = createElement( | ||
'dialog', | ||
@@ -1079,3 +1084,3 @@ { | ||
createElement('h2', { className: 'dialog__header' }, options.formTitle), | ||
$form, | ||
formEl, | ||
), | ||
@@ -1085,3 +1090,3 @@ ); | ||
return { | ||
$el, | ||
el, | ||
showError, | ||
@@ -1105,6 +1110,5 @@ hideError, | ||
function SuccessIcon() { | ||
const cENS = (tagName) => | ||
// eslint-disable-next-line no-restricted-globals | ||
document.createElementNS(XMLNS, tagName); | ||
const svg = setAttributesNS(cENS('svg'), { | ||
const createElementNS = (tagName) => | ||
WINDOW.document.createElementNS(XMLNS, tagName); | ||
const svg = setAttributesNS(createElementNS('svg'), { | ||
class: 'success-icon', | ||
@@ -1117,7 +1121,7 @@ width: `${WIDTH}`, | ||
const g = setAttributesNS(cENS('g'), { | ||
const g = setAttributesNS(createElementNS('g'), { | ||
clipPath: 'url(#clip0_57_156)', | ||
}); | ||
const path2 = setAttributesNS(cENS('path'), { | ||
const path2 = setAttributesNS(createElementNS('path'), { | ||
['fill-rule']: 'evenodd', | ||
@@ -1127,3 +1131,3 @@ ['clip-rule']: 'evenodd', | ||
}); | ||
const path = setAttributesNS(cENS('path'), { | ||
const path = setAttributesNS(createElementNS('path'), { | ||
d: 'M6.68775 12.4297C6.78586 12.4745 6.89218 12.4984 7 12.5C7.11275 12.4955 7.22315 12.4664 7.32337 12.4145C7.4236 12.3627 7.51121 12.2894 7.58 12.2L12 5.63999C12.0848 5.47724 12.1071 5.28902 12.0625 5.11098C12.0178 4.93294 11.9095 4.77744 11.7579 4.67392C11.6064 4.57041 11.4221 4.52608 11.24 4.54931C11.0579 4.57254 10.8907 4.66173 10.77 4.79999L6.88 10.57L5.13 8.56999C5.06508 8.49566 4.98613 8.43488 4.89768 8.39111C4.80922 8.34735 4.713 8.32148 4.61453 8.31498C4.51605 8.30847 4.41727 8.32147 4.32382 8.35322C4.23038 8.38497 4.14413 8.43484 4.07 8.49999C3.92511 8.63217 3.83692 8.81523 3.82387 9.01092C3.81083 9.2066 3.87393 9.39976 4 9.54999L6.43 12.24C6.50187 12.3204 6.58964 12.385 6.68775 12.4297Z', | ||
@@ -1134,8 +1138,8 @@ }); | ||
const speakerDefs = cENS('defs'); | ||
const speakerClipPathDef = setAttributesNS(cENS('clipPath'), { | ||
const speakerDefs = createElementNS('defs'); | ||
const speakerClipPathDef = setAttributesNS(createElementNS('clipPath'), { | ||
id: 'clip0_57_156', | ||
}); | ||
const speakerRect = setAttributesNS(cENS('rect'), { | ||
const speakerRect = setAttributesNS(createElementNS('rect'), { | ||
width: `${WIDTH}`, | ||
@@ -1153,3 +1157,3 @@ height: `${WIDTH}`, | ||
return { | ||
$el: svg, | ||
el: svg, | ||
}; | ||
@@ -1163,11 +1167,11 @@ } | ||
function remove() { | ||
if (!$el) { | ||
if (!el) { | ||
return; | ||
} | ||
$el.remove(); | ||
el.remove(); | ||
onRemove && onRemove(); | ||
} | ||
const $el = createElement( | ||
const el = createElement( | ||
'div', | ||
@@ -1178,3 +1182,3 @@ { | ||
}, | ||
SuccessIcon().$el, | ||
SuccessIcon().el, | ||
message, | ||
@@ -1184,3 +1188,3 @@ ); | ||
return { | ||
$el, | ||
el, | ||
remove, | ||
@@ -1191,3 +1195,3 @@ }; | ||
/** | ||
* | ||
* Creates a new widget. Returns public methods that control widget behavior. | ||
*/ | ||
@@ -1218,3 +1222,3 @@ function createWidget({ shadow, options, attachTo }) { | ||
shadow.appendChild(success.$el); | ||
shadow.appendChild(success.el); | ||
@@ -1241,3 +1245,3 @@ const timeoutId = setTimeout(() => { | ||
const result = await handleFeedbackSubmit(dialog, feedback); | ||
const result = await handleFeedbackSubmit(dialog, feedback, { referrer: options.referrer }); | ||
@@ -1280,3 +1284,3 @@ // Error submitting feedback | ||
function removeActor() { | ||
actor && actor.$el.remove(); | ||
actor && actor.el.remove(); | ||
} | ||
@@ -1317,3 +1321,3 @@ | ||
shadow.appendChild(dialog.$el); | ||
shadow.appendChild(dialog.el); | ||
@@ -1352,3 +1356,3 @@ // Hides the default actor whenever dialog is opened | ||
hideDialog(); | ||
dialog.$el.remove(); | ||
dialog.el.remove(); | ||
dialog = undefined; | ||
@@ -1377,3 +1381,3 @@ } | ||
actor = Actor({ options, onClick: handleActorClick }); | ||
shadow.appendChild(actor.$el); | ||
shadow.appendChild(actor.el); | ||
} else { | ||
@@ -1397,13 +1401,3 @@ attachTo.addEventListener('click', handleActorClick); | ||
// Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them | ||
function isElectronNodeRenderer() { | ||
return typeof process !== 'undefined' && (process ).type === 'renderer'; | ||
} | ||
/** | ||
* Returns true if we are in the browser. | ||
*/ | ||
function isBrowser() { | ||
// eslint-disable-next-line no-restricted-globals | ||
return typeof window !== 'undefined' && (!isNodeEnv() || isElectronNodeRenderer()); | ||
} | ||
const doc = WINDOW.document; | ||
@@ -1451,3 +1445,2 @@ /** | ||
id = 'sentry-feedback', | ||
// attachTo = null, | ||
autoInject = true, | ||
@@ -1498,3 +1491,2 @@ showEmail = true, | ||
id, | ||
// attachTo, | ||
autoInject, | ||
@@ -1545,4 +1537,3 @@ isAnonymous, | ||
} | ||
// eslint-disable-next-line no-restricted-globals | ||
const existingFeedback = document.querySelector(`#${this.options.id}`); | ||
const existingFeedback = doc.querySelector(`#${this.options.id}`); | ||
if (existingFeedback) { | ||
@@ -1562,3 +1553,2 @@ existingFeedback.remove(); | ||
} catch (err) { | ||
// TODO: error handling | ||
logger.error(err); | ||
@@ -1575,6 +1565,5 @@ } | ||
return this._ensureShadowHost(options, ([shadow]) => { | ||
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; | ||
@@ -1626,2 +1615,3 @@ if (!targetEl) { | ||
} | ||
return false; | ||
@@ -1655,8 +1645,7 @@ } | ||
_createWidget(options) { | ||
return this._ensureShadowHost(options, ([shadow]) => { | ||
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)); | ||
shadow.appendChild(createActorStyles(doc)); | ||
this._hasInsertedActorStyles = true; | ||
@@ -1680,4 +1669,4 @@ } | ||
// Don't create if it already exists | ||
if (!this._shadow && !this._host) { | ||
const [shadow, host] = createShadowHost({ options }); | ||
if (!this._shadow || !this._host) { | ||
const { shadow, host } = createShadowHost({ options }); | ||
this._shadow = shadow; | ||
@@ -1688,16 +1677,9 @@ this._host = host; | ||
if (!this._shadow || !this._host) { | ||
logger.warn('[Feedback] Unable to create host element and/or shadow DOM'); | ||
// This shouldn't happen | ||
return null; | ||
} | ||
// set data attribute on host for different themes | ||
this._host.dataset.sentryFeedbackColorscheme = options.colorScheme; | ||
const result = cb([this._shadow, this._host]); | ||
const result = cb({ shadow: this._shadow, host: this._host }); | ||
if (needsAppendHost) { | ||
// eslint-disable-next-line no-restricted-globals | ||
document.body.appendChild(this._host); | ||
doc.body.appendChild(this._host); | ||
} | ||
@@ -1704,0 +1686,0 @@ |
import { Integration } from '@sentry/types'; | ||
import { FeedbackConfigurationWithDefaults, Widget } from './types'; | ||
import { CreateWidgetOptionOverrides, FeedbackConfigurationWithDefaults, Widget } from './types'; | ||
import { createShadowHost } from './widget/createShadowHost'; | ||
@@ -51,7 +51,7 @@ type FeedbackConfiguration = Partial<FeedbackConfigurationWithDefaults>; | ||
*/ | ||
attachTo(el: Node | string, optionOverrides: Partial<FeedbackConfigurationWithDefaults>): Widget | null; | ||
attachTo(el: Node | string, optionOverrides: CreateWidgetOptionOverrides): Widget | null; | ||
/** | ||
* Creates a new widget. Accepts partial options to override any options passed to constructor. | ||
*/ | ||
createWidget(optionOverrides: Partial<FeedbackConfigurationWithDefaults>): Widget | null; | ||
createWidget(optionOverrides: CreateWidgetOptionOverrides): Widget | null; | ||
/** | ||
@@ -58,0 +58,0 @@ * Removes a single widget |
@@ -0,1 +1,2 @@ | ||
import { SendFeedbackOptions } from './types'; | ||
import { sendFeedbackRequest } from './util/sendFeedbackRequest'; | ||
@@ -8,10 +9,7 @@ interface SendFeedbackParams { | ||
} | ||
interface SendFeedbackOptions { | ||
includeReplay?: boolean; | ||
} | ||
/** | ||
* Public API to send a Feedback item to Sentry | ||
*/ | ||
export declare function sendFeedback({ name, email, message, url }: SendFeedbackParams, { includeReplay }?: SendFeedbackOptions): ReturnType<typeof sendFeedbackRequest>; | ||
export declare function sendFeedback({ name, email, message, url }: SendFeedbackParams, { referrer, includeReplay }?: SendFeedbackOptions): ReturnType<typeof sendFeedbackRequest>; | ||
export {}; | ||
//# sourceMappingURL=sendFeedback.d.ts.map |
@@ -28,3 +28,17 @@ import { Event, Primitive } from '@sentry/types'; | ||
}; | ||
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 { | ||
@@ -188,2 +202,5 @@ message: string; | ||
} | ||
export interface CreateWidgetOptionOverrides extends Partial<FeedbackConfigurationWithDefaults> { | ||
referrer?: string; | ||
} | ||
export interface FeedbackThemes { | ||
@@ -194,3 +211,3 @@ dark: FeedbackTheme; | ||
export interface FeedbackComponent<T extends HTMLElement> { | ||
$el: T; | ||
el: T; | ||
} | ||
@@ -197,0 +214,0 @@ /** |
@@ -1,7 +0,7 @@ | ||
import { FeedbackFormData } from '../types'; | ||
import { FeedbackFormData, SendFeedbackOptions } from '../types'; | ||
import { DialogComponent } from '../widget/Dialog'; | ||
/** | ||
* | ||
* Calls `sendFeedback` to send feedback, handles UI behavior of dialog. | ||
*/ | ||
export declare function handleFeedbackSubmit(dialog: DialogComponent | null, feedback: FeedbackFormData): Promise<Response | false>; | ||
export declare function handleFeedbackSubmit(dialog: DialogComponent | null, feedback: FeedbackFormData, options?: SendFeedbackOptions): Promise<Response | false>; | ||
//# sourceMappingURL=handleFeedbackSubmit.d.ts.map |
@@ -5,3 +5,3 @@ import { SendFeedbackData } from '../types'; | ||
*/ | ||
export declare function sendFeedbackRequest({ feedback: { message, email, name, replay_id, url }, }: SendFeedbackData): Promise<Response | null>; | ||
export declare function sendFeedbackRequest({ feedback: { message, email, name, replay_id, url }, referrer, }: SendFeedbackData): Promise<Response | null>; | ||
//# sourceMappingURL=sendFeedbackRequest.d.ts.map |
@@ -6,9 +6,9 @@ import { FeedbackConfigurationWithDefaults } from '../types'; | ||
/** | ||
* | ||
* Creates shadow host | ||
*/ | ||
export declare function createShadowHost({ options }: CreateShadowHostParams): [ | ||
/*shadow*/ ShadowRoot, | ||
/*host*/ HTMLDivElement | ||
]; | ||
export declare function createShadowHost({ options }: CreateShadowHostParams): { | ||
shadow: ShadowRoot; | ||
host: HTMLDivElement; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=createShadowHost.d.ts.map |
import { FeedbackConfigurationWithDefaults, Widget } from '../types'; | ||
interface CreateWidgetParams { | ||
shadow: ShadowRoot; | ||
options: FeedbackConfigurationWithDefaults; | ||
options: FeedbackConfigurationWithDefaults & { | ||
referrer?: string; | ||
}; | ||
attachTo?: Node; | ||
} | ||
/** | ||
* | ||
* Creates a new widget. Returns public methods that control widget behavior. | ||
*/ | ||
@@ -10,0 +12,0 @@ export declare function createWidget({ shadow, options, attachTo }: CreateWidgetParams): Widget; |
interface IconReturn { | ||
$el: SVGElement; | ||
el: SVGElement; | ||
} | ||
@@ -4,0 +4,0 @@ /** |
interface IconReturn { | ||
$el: SVGElement; | ||
el: SVGElement; | ||
} | ||
@@ -4,0 +4,0 @@ /** |
import type { Integration } from '@sentry/types'; | ||
import type { FeedbackConfigurationWithDefaults, Widget } from './types'; | ||
import type { CreateWidgetOptionOverrides, FeedbackConfigurationWithDefaults, Widget } from './types'; | ||
import { createShadowHost } from './widget/createShadowHost'; | ||
@@ -51,7 +51,7 @@ type FeedbackConfiguration = Partial<FeedbackConfigurationWithDefaults>; | ||
*/ | ||
attachTo(el: Node | string, optionOverrides: Partial<FeedbackConfigurationWithDefaults>): Widget | null; | ||
attachTo(el: Node | string, optionOverrides: CreateWidgetOptionOverrides): Widget | null; | ||
/** | ||
* Creates a new widget. Accepts partial options to override any options passed to constructor. | ||
*/ | ||
createWidget(optionOverrides: Partial<FeedbackConfigurationWithDefaults>): Widget | null; | ||
createWidget(optionOverrides: CreateWidgetOptionOverrides): Widget | null; | ||
/** | ||
@@ -58,0 +58,0 @@ * Removes a single widget |
@@ -0,1 +1,2 @@ | ||
import type { SendFeedbackOptions } from './types'; | ||
import { sendFeedbackRequest } from './util/sendFeedbackRequest'; | ||
@@ -8,10 +9,7 @@ interface SendFeedbackParams { | ||
} | ||
interface SendFeedbackOptions { | ||
includeReplay?: boolean; | ||
} | ||
/** | ||
* Public API to send a Feedback item to Sentry | ||
*/ | ||
export declare function sendFeedback({ name, email, message, url }: SendFeedbackParams, { includeReplay }?: SendFeedbackOptions): ReturnType<typeof sendFeedbackRequest>; | ||
export declare function sendFeedback({ name, email, message, url }: SendFeedbackParams, { referrer, includeReplay }?: SendFeedbackOptions): ReturnType<typeof sendFeedbackRequest>; | ||
export {}; | ||
//# sourceMappingURL=sendFeedback.d.ts.map |
@@ -28,3 +28,17 @@ import type { Event, Primitive } from '@sentry/types'; | ||
}; | ||
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 { | ||
@@ -188,2 +202,5 @@ message: string; | ||
} | ||
export interface CreateWidgetOptionOverrides extends Partial<FeedbackConfigurationWithDefaults> { | ||
referrer?: string; | ||
} | ||
export interface FeedbackThemes { | ||
@@ -194,3 +211,3 @@ dark: FeedbackTheme; | ||
export interface FeedbackComponent<T extends HTMLElement> { | ||
$el: T; | ||
el: T; | ||
} | ||
@@ -197,0 +214,0 @@ /** |
@@ -1,7 +0,7 @@ | ||
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 declare function handleFeedbackSubmit(dialog: DialogComponent | null, feedback: FeedbackFormData): Promise<Response | false>; | ||
export declare function handleFeedbackSubmit(dialog: DialogComponent | null, feedback: FeedbackFormData, options?: SendFeedbackOptions): Promise<Response | false>; | ||
//# sourceMappingURL=handleFeedbackSubmit.d.ts.map |
@@ -5,3 +5,3 @@ import type { SendFeedbackData } from '../types'; | ||
*/ | ||
export declare function sendFeedbackRequest({ feedback: { message, email, name, replay_id, url }, }: SendFeedbackData): Promise<Response | null>; | ||
export declare function sendFeedbackRequest({ feedback: { message, email, name, replay_id, url }, referrer, }: SendFeedbackData): Promise<Response | null>; | ||
//# sourceMappingURL=sendFeedbackRequest.d.ts.map |
@@ -6,6 +6,9 @@ import type { FeedbackConfigurationWithDefaults } from '../types'; | ||
/** | ||
* | ||
* Creates shadow host | ||
*/ | ||
export declare function createShadowHost({ options }: CreateShadowHostParams): [shadow: ShadowRoot, host: HTMLDivElement]; | ||
export declare function createShadowHost({ options }: CreateShadowHostParams): { | ||
shadow: ShadowRoot; | ||
host: HTMLDivElement; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=createShadowHost.d.ts.map |
import type { FeedbackConfigurationWithDefaults, Widget } from '../types'; | ||
interface CreateWidgetParams { | ||
shadow: ShadowRoot; | ||
options: FeedbackConfigurationWithDefaults; | ||
options: FeedbackConfigurationWithDefaults & { | ||
referrer?: string; | ||
}; | ||
attachTo?: Node; | ||
} | ||
/** | ||
* | ||
* Creates a new widget. Returns public methods that control widget behavior. | ||
*/ | ||
@@ -10,0 +12,0 @@ export declare function createWidget({ shadow, options, attachTo }: CreateWidgetParams): Widget; |
interface IconReturn { | ||
$el: SVGElement; | ||
el: SVGElement; | ||
} | ||
@@ -4,0 +4,0 @@ /** |
interface IconReturn { | ||
$el: SVGElement; | ||
el: SVGElement; | ||
} | ||
@@ -4,0 +4,0 @@ /** |
{ | ||
"name": "@sentry-internal/feedback", | ||
"version": "0.0.1-alpha.4", | ||
"version": "0.0.1-alpha.5", | ||
"description": "Sentry SDK integration for user feedback", | ||
@@ -26,6 +26,6 @@ "repository": "git://github.com/getsentry/sentry-javascript.git", | ||
"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" | ||
}, | ||
@@ -32,0 +32,0 @@ "scripts": { |
185
README.md
@@ -14,19 +14,24 @@ <p align="center"> | ||
`@sentry/feedback` currently can only be used by browsers with [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) support. | ||
`@sentry-internal/feedback` currently can only be used by browsers with [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) support. | ||
## Installation | ||
Feedback can be imported from `@sentry/browser`, or a respective SDK package like `@sentry/react` or `@sentry/vue`. | ||
You don't need to install anything in order to use Feedback. The minimum version that includes Feedback is <<CHANGEME>>. | ||
During the alpha phase, the feedback integration will need to be imported from `@sentry-internal/feedback`. This will be | ||
changed for the general release. | ||
For details on using Feedback when using Sentry via the CDN bundles, see [CDN bundle](#loading-feedback-as-a-cdn-bundle). | ||
```shell | ||
npm add @sentry-internal/feedback | ||
``` | ||
## Setup | ||
To set up the integration, add the following to your Sentry initialization. Several options are supported and passable via the integration constructor. | ||
See the [configuration section](#configuration) below for more details. | ||
To set up the integration, add the following to your Sentry initialization. This will inject a feedback button to the bottom right corner of your application. Users can then click it to open up a feedback form where they can submit feedback. | ||
Several options are supported and passable via the integration constructor. See the [configuration section](#configuration) below for more details. | ||
```javascript | ||
import * as Sentry from '@sentry/browser'; | ||
// or e.g. import * as Sentry from '@sentry/react'; | ||
// or from a framework specific SDK, e.g. | ||
// import * as Sentry from '@sentry/react'; | ||
import Feedback from '@sentry-internal/feedback'; | ||
@@ -36,3 +41,3 @@ Sentry.init({ | ||
integrations: [ | ||
new Sentry.Feedback({ | ||
new Feedback({ | ||
// Additional SDK configuration goes in here, for example: | ||
@@ -46,57 +51,157 @@ // See below for all available options | ||
### Lazy loading Feedback | ||
## Configuration | ||
Feedback will start automatically when you add the integration. | ||
If you do not want to start Feedback immediately (e.g. if you want to lazy-load it), | ||
you can also use `addIntegration` to load it later: | ||
### General Integration Configuration | ||
```js | ||
import * as Sentry from "@sentry/react"; | ||
import { BrowserClient } from "@sentry/browser"; | ||
The following options can be configured as options to the integration, in `new Feedback({})`: | ||
Sentry.init({ | ||
// Do not load it initially | ||
integrations: [] | ||
| key | type | default | description | | ||
| --------- | ------- | ------- | ----------- | | ||
| `autoInject` | `boolean` | `true` | Injects the Feedback widget into the application when the integration is added. This is useful to turn off if you bring your own button, or only want to show the widget on certain views. | | ||
| `colorScheme` | `"system" \| "light" \| "dark"` | `"system"` | The color theme to use. `"system"` will follow your OS colorscheme. | | ||
### User/form Related Configuration | ||
| key | type | default | description | | ||
| --------- | ------- | ------- | ----------- | | ||
| `showName` | `boolean` | `true` | Displays the name field on the feedback form, however will still capture the name (if available) from Sentry SDK context. | | ||
| `showEmail` | `boolean` | `true` | Displays the email field on the feedback form, however will still capture the email (if available) from Sentry SDK context. | | ||
| `isAnonymous` | `boolean` | `false` | Hides both name and email fields and does not use Sentry SDK's user context. | | ||
| `useSentryUser` | `Record<string, string>` | `{ email: 'email', name: 'username'}` | Map of the `email` and `name` fields to the corresponding Sentry SDK user fields that were called with `Sentry.setUser`. | | ||
By default the Feedback integration will attempt to fill in the name/email fields if you have set a user context via [`Sentry.setUser`](https://docs.sentry.io/platforms/javascript/enriching-events/identify-user/). By default it expects the email and name fields to be `email` and `username`. Below is an example configuration with non-default user fields. | ||
```javascript | ||
Sentry.setUser({ | ||
email: 'foo@example.com', | ||
fullName: 'Jane Doe', | ||
}); | ||
// Sometime later | ||
const { Feedback } = await import('@sentry/browser'); | ||
const client = Sentry.getCurrentHub().getClient<BrowserClient>(); | ||
// Client can be undefined | ||
client?.addIntegration(new Feedback()); | ||
new Feedback({ | ||
useSentryUser({ | ||
email: 'email', | ||
name: 'fullName', | ||
}), | ||
}) | ||
``` | ||
### Identifying Users | ||
### Text Customization | ||
Most text that you see in the default Feedback widget can be customized. | ||
If you have only followed the above instructions to setup session feedbacks, you will only see IP addresses in Sentry's UI. In order to associate a user identity to a session feedback, use [`setUser`](https://docs.sentry.io/platforms/javascript/enriching-events/identify-user/). | ||
| key | default | description | | ||
| --------- | ------- | ----------- | | ||
| `buttonLabel` | `"Feedback"` | The label of the widget button. | | ||
| `submitButtonLabel` | `"Send Feedback"` | The label of the submit button used in the feedback form dialog. | | ||
| `cancelButtonLabel` | `"Cancel"` | The label of the cancel button used in the feedback form dialog. | | ||
| `formTitle` | `"Send Feedback"` | The title at the top of the feedback form dialog. | | ||
| `nameLabel` | `"Full Name"` | The label of the name input field. | | ||
| `namePlaceholder` | `"Full Name"` | The placeholder for the name input field. | | ||
| `emailLabel` | `"Email"` | The label of the email input field. || | ||
| `emailPlaceholder` | `"Email"` | The placeholder for the email input field. | | ||
| `messageLabel` | `"Description"` | The label for the feedback description input field. | | ||
| `messagePlaceholder` | `"What's the issue? What did you expect?"` | The placeholder for the feedback description input field. | | ||
| `successMessageText` | `"Thank you for your report!"` | The message to be displayed after a succesful feedback submission. | | ||
```javascript | ||
import * as Sentry from "@sentry/browser"; | ||
new Feedback({ | ||
buttonLabel: 'Bug Report', | ||
submitButtonLabel: 'Send Report', | ||
formTitle: 'Send Bug Report', | ||
}); | ||
``` | ||
Sentry.setUser({ email: "jane.doe@example.com" }); | ||
### Theme Customization | ||
Colors can be customized via the Feedback constructor or by defining CSS variables on the widget button. If you use the default widget button, it will have an `id="sentry-feedback`, meaning you can use the `#sentry-feedback` selector to define CSS variables to override. | ||
| key | css variable | light | dark | description | | ||
| --- | --- | --- | --- | --- | | ||
| `background` | `--bg-color` | `#ffffff` | `#29232f` | Background color of the widget actor and dialog. | | ||
| `backgroundHover` | `--bg-hover-color` | `#f6f6f7` | `#352f3b` | The background color of widget actor when in a hover state | | ||
| `foreground` | `--fg-color` | `#2b2233` | `#ebe6ef` | The foreground color, e.g. text color | | ||
| `error` | `--error-color` | `#df3338` | `#f55459` | Color used for error related components (e.g. text color when there was an error submitting feedback) | | ||
| `success` | `--success-color` | `#268d75` | `#2da98c` | Color used for success-related components (e.g. text color when feedback is submitted successfully) | | ||
| `border` | `--border` | `1.5px solid rgba(41, 35, 47, 0.13)` | `1.5px solid rgba(235, 230, 239, 0.15)` | The border style used for the widget actor and dialog | | ||
| `boxShadow` | `--box-shadow` | `0px 4px 24px 0px rgba(43, 34, 51, 0.12)` | `0px 4px 24px 0px rgba(43, 34, 51, 0.12)` | The box shadow style used for the widget actor and dialog | | ||
Here is an example of customizing only the background color for the light theme using the Feedback constructor configuration. | ||
```javascript | ||
new Feedback({ | ||
themeLight: { | ||
background: "#cccccc", | ||
}, | ||
}) | ||
``` | ||
## Loading Feedback as a CDN Bundle | ||
Or the same example above but using the CSS variables method: | ||
As an alternative to the NPM package, you can use Feedback as a CDN bundle. | ||
Please refer to the [Feedback installation guide](https://docs.sentry.io/platforms/javascript/session-feedback/#install) for CDN bundle instructions. | ||
```css | ||
#sentry-feedback { | ||
--bg-color: #cccccc; | ||
} | ||
``` | ||
### Additional UI Customization | ||
Similar to theme customization above, these are additional CSS variables that can be overridden. Note these are not supported in the constructor. | ||
## Configuration | ||
| Variable | Default | Description | | ||
| --- | --- | --- | | ||
| `--bottom` | `1rem` | By default the widget has a position of fixed, and is in the bottom right corner. | | ||
| `--right` | `1rem` | By default the widget has a position of fixed, and is in the bottom right corner. | | ||
| `--top` | `auto` | By default the widget has a position of fixed, and is in the bottom right corner. | | ||
| `--left` | `auto` | By default the widget has a position of fixed, and is in the bottom right corner. | | ||
| `--z-index` | `100000` | The z-index of the widget | | ||
| `--font-family` | `"'Helvetica Neue', Arial, sans-serif"` | Default font-family to use| | ||
| `--font-size` | `14px` | Font size | | ||
### General Integration Configuration | ||
### Event Callbacks | ||
Sometimes it’s important to know when someone has started to interact with the feedback form, so you can add custom logging, or start/stop background timers on the page until the user is done. | ||
The following options can be configured as options to the integration, in `new Feedback({})`: | ||
Pass these callbacks when you initialize the Feedback integration: | ||
| key | type | default | description | | ||
| --------- | ------- | ------- | ----------- | | ||
| tbd | boolean | `true` | tbd | | ||
```javascript | ||
new Feedback({ | ||
onActorClick: () => {}, | ||
onDialogOpen: () => {}, | ||
onDialogClose: () => {}, | ||
onSubmitSuccess: () => {}, | ||
onSubmitError: () => {}, | ||
}); | ||
``` | ||
## Further Customization | ||
There are two more methods in the integration that can help customization. | ||
### Bring Your Own Button | ||
## Manually Sending Feedback Data | ||
You can skip the default widget button and use your own button. Call `feedback.attachTo()` to have the SDK attach a click listener to your own button. You can additionally supply the same customization options that the constructor accepts (e.g. for text labels and colors). | ||
Connect your own feedback UI to Sentry's You can use `feedback.flush()` to immediately send all currently captured feedback data. | ||
When Feedback is currently in buffering mode, this will send up to the last 60 seconds of feedback data, | ||
and also continue sending afterwards, similar to when an error happens & is recorded. | ||
```javascript | ||
const feedback = new Feedback({ | ||
// Disable injecting the default widget | ||
autoInject: false, | ||
}); | ||
feedback.attachTo(document.querySelector('#your-button'), { | ||
formTitle: "Report a Bug!" | ||
}); | ||
``` | ||
### Bring Your Own Widget | ||
You can also bring your own widget and UI and simply pass a feedback object to the `sendFeedback()` function. | ||
```html | ||
<form id="my-feedback-form> | ||
<input name="name" /> | ||
<input name="email" /> | ||
<textarea name="message" placeholder="What's the issue?" /> | ||
</form> | ||
<script> | ||
document.getElementById('my-feedback-form').addEventListener('submit', (event) => { | ||
const formData = new FormData(event.currentTarget); | ||
Feedback.sendFeedback(formData); | ||
event.preventDefault(); | ||
}); | ||
</script> | ||
``` |
@@ -0,3 +1,4 @@ | ||
import { WINDOW } from '@sentry/browser'; | ||
import type { Integration } from '@sentry/types'; | ||
import { isNodeEnv, logger } from '@sentry/utils'; | ||
import { isBrowser, logger } from '@sentry/utils'; | ||
@@ -18,3 +19,3 @@ import { | ||
} from './constants'; | ||
import type { FeedbackConfigurationWithDefaults, Widget } from './types'; | ||
import type { CreateWidgetOptionOverrides, FeedbackConfigurationWithDefaults, Widget } from './types'; | ||
import { createActorStyles } from './widget/Actor.css'; | ||
@@ -24,16 +25,4 @@ import { createShadowHost } from './widget/createShadowHost'; | ||
type ElectronProcess = { type?: string }; | ||
const doc = WINDOW.document; | ||
// 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<FeedbackConfigurationWithDefaults>; | ||
@@ -89,3 +78,2 @@ | ||
id = 'sentry-feedback', | ||
// attachTo = null, | ||
autoInject = true, | ||
@@ -136,3 +124,2 @@ showEmail = true, | ||
id, | ||
// attachTo, | ||
autoInject, | ||
@@ -183,4 +170,3 @@ isAnonymous, | ||
} | ||
// eslint-disable-next-line no-restricted-globals | ||
const existingFeedback = document.querySelector(`#${this.options.id}`); | ||
const existingFeedback = doc.querySelector(`#${this.options.id}`); | ||
if (existingFeedback) { | ||
@@ -200,3 +186,2 @@ existingFeedback.remove(); | ||
} catch (err) { | ||
// TODO: error handling | ||
logger.error(err); | ||
@@ -209,10 +194,9 @@ } | ||
*/ | ||
public attachTo(el: Node | string, optionOverrides: Partial<FeedbackConfigurationWithDefaults>): Widget | null { | ||
public attachTo(el: Node | string, optionOverrides: CreateWidgetOptionOverrides): Widget | null { | ||
try { | ||
const options = Object.assign({}, this.options, optionOverrides); | ||
return this._ensureShadowHost<Widget | null>(options, ([shadow]) => { | ||
return this._ensureShadowHost<Widget | null>(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; | ||
@@ -237,3 +221,3 @@ if (!targetEl) { | ||
*/ | ||
public createWidget(optionOverrides: Partial<FeedbackConfigurationWithDefaults>): Widget | null { | ||
public createWidget(optionOverrides: CreateWidgetOptionOverrides): Widget | null { | ||
try { | ||
@@ -265,2 +249,3 @@ return this._createWidget(Object.assign({}, this.options, optionOverrides)); | ||
} | ||
return false; | ||
@@ -294,8 +279,7 @@ } | ||
protected _createWidget(options: FeedbackConfigurationWithDefaults): Widget | null { | ||
return this._ensureShadowHost<Widget>(options, ([shadow]) => { | ||
return this._ensureShadowHost<Widget>(options, ({ shadow }) => { | ||
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; | ||
@@ -319,4 +303,4 @@ } | ||
// Don't create if it already exists | ||
if (!this._shadow && !this._host) { | ||
const [shadow, host] = createShadowHost({ options }); | ||
if (!this._shadow || !this._host) { | ||
const { shadow, host } = createShadowHost({ options }); | ||
this._shadow = shadow; | ||
@@ -327,16 +311,9 @@ this._host = host; | ||
if (!this._shadow || !this._host) { | ||
logger.warn('[Feedback] Unable to create host element and/or shadow DOM'); | ||
// This shouldn't happen | ||
return null; | ||
} | ||
// set data attribute on host for different themes | ||
this._host.dataset.sentryFeedbackColorscheme = options.colorScheme; | ||
const result = cb([this._shadow, this._host]); | ||
const result = cb({ shadow: this._shadow, host: this._host }); | ||
if (needsAppendHost) { | ||
// eslint-disable-next-line no-restricted-globals | ||
document.body.appendChild(this._host); | ||
doc.body.appendChild(this._host); | ||
} | ||
@@ -343,0 +320,0 @@ |
@@ -5,2 +5,3 @@ import type { BrowserClient, Replay } from '@sentry/browser'; | ||
import type { SendFeedbackOptions } from './types'; | ||
import { sendFeedbackRequest } from './util/sendFeedbackRequest'; | ||
@@ -15,6 +16,2 @@ | ||
interface SendFeedbackOptions { | ||
includeReplay?: boolean; | ||
} | ||
/** | ||
@@ -25,3 +22,3 @@ * Public API to send a Feedback item to Sentry | ||
{ name, email, message, url = getLocationHref() }: SendFeedbackParams, | ||
{ includeReplay = true }: SendFeedbackOptions = {}, | ||
{ referrer, includeReplay = true }: SendFeedbackOptions = {}, | ||
): ReturnType<typeof sendFeedbackRequest> { | ||
@@ -44,3 +41,4 @@ const hub = getCurrentHub(); | ||
}, | ||
referrer, | ||
}); | ||
} |
@@ -32,4 +32,20 @@ import type { Event, Primitive } from '@sentry/types'; | ||
}; | ||
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 { | ||
@@ -217,2 +233,6 @@ message: string; | ||
export interface CreateWidgetOptionOverrides extends Partial<FeedbackConfigurationWithDefaults> { | ||
referrer?: string; | ||
} | ||
export interface FeedbackThemes { | ||
@@ -224,3 +244,3 @@ dark: FeedbackTheme; | ||
export interface FeedbackComponent<T extends HTMLElement> { | ||
$el: T; | ||
el: T; | ||
} | ||
@@ -227,0 +247,0 @@ |
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. | ||
*/ | ||
@@ -11,2 +11,3 @@ export async function handleFeedbackSubmit( | ||
feedback: FeedbackFormData, | ||
options?: SendFeedbackOptions, | ||
): Promise<Response | false> { | ||
@@ -29,3 +30,3 @@ if (!dialog) { | ||
dialog.setSubmitDisabled(); | ||
const resp = await sendFeedback(feedback); | ||
const resp = await sendFeedback(feedback, options); | ||
@@ -32,0 +33,0 @@ if (!resp) { |
@@ -12,2 +12,3 @@ import { getCurrentHub } from '@sentry/core'; | ||
feedback: { message, email, name, replay_id, url }, | ||
referrer, | ||
}: SendFeedbackData): Promise<Response | null> { | ||
@@ -37,2 +38,5 @@ const hub = getCurrentHub(); | ||
}, | ||
tags: { | ||
referrer, | ||
}, | ||
// type: 'feedback_event', | ||
@@ -39,0 +43,0 @@ }; |
import type { FeedbackComponent, FeedbackConfigurationWithDefaults } from '../types'; | ||
import { Icon } from './Icon'; | ||
import { createElement as h } from './util/createElement'; | ||
import { createElement } from './util/createElement'; | ||
@@ -29,3 +29,3 @@ interface Props { | ||
const $el = h( | ||
const el = createElement( | ||
'button', | ||
@@ -37,4 +37,4 @@ { | ||
}, | ||
Icon().$el, | ||
h( | ||
Icon().el, | ||
createElement( | ||
'span', | ||
@@ -48,13 +48,13 @@ { | ||
$el.addEventListener('click', _handleClick); | ||
el.addEventListener('click', _handleClick); | ||
return { | ||
$el, | ||
el, | ||
show: (): void => { | ||
$el.classList.remove('widget__actor--hidden'); | ||
el.classList.remove('widget__actor--hidden'); | ||
}, | ||
hide: (): void => { | ||
$el.classList.add('widget__actor--hidden'); | ||
el.classList.add('widget__actor--hidden'); | ||
}, | ||
}; | ||
} |
@@ -0,1 +1,2 @@ | ||
import { WINDOW } from '@sentry/browser'; | ||
import { logger } from '@sentry/utils'; | ||
@@ -10,27 +11,28 @@ | ||
} | ||
/** | ||
* | ||
* Creates shadow host | ||
*/ | ||
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.'); | ||
} | ||
export function createShadowHost({ options }: CreateShadowHostParams): { shadow: ShadowRoot; host: HTMLDivElement } { | ||
try { | ||
const doc = WINDOW.document; | ||
// Create the host | ||
const host = doc.createElement('div'); | ||
host.id = options.id; | ||
// Create the host | ||
const host = doc.createElement('div'); | ||
host.id = options.id; | ||
// Create the shadow root | ||
const shadow = host.attachShadow({ mode: 'open' }); | ||
// 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)); | ||
shadow.appendChild( | ||
createMainStyles(doc, options.colorScheme, { dark: options.themeDark, light: options.themeLight }), | ||
); | ||
shadow.appendChild(createDialogStyles(doc)); | ||
return [shadow, host]; | ||
return { shadow, host }; | ||
} catch { | ||
// Shadow DOM probably not supported | ||
logger.warn('[Feedback] Browser does not support shadow DOM API'); | ||
throw new Error('Browser does not support shadow DOM API.'); | ||
} | ||
} |
@@ -14,3 +14,3 @@ import { getCurrentHub } from '@sentry/core'; | ||
shadow: ShadowRoot; | ||
options: FeedbackConfigurationWithDefaults; | ||
options: FeedbackConfigurationWithDefaults & { referrer?: string }; | ||
attachTo?: Node; | ||
@@ -20,3 +20,3 @@ } | ||
/** | ||
* | ||
* Creates a new widget. Returns public methods that control widget behavior. | ||
*/ | ||
@@ -47,3 +47,3 @@ export function createWidget({ shadow, options, attachTo }: CreateWidgetParams): Widget { | ||
shadow.appendChild(success.$el); | ||
shadow.appendChild(success.el); | ||
@@ -70,3 +70,3 @@ const timeoutId = setTimeout(() => { | ||
const result = await handleFeedbackSubmit(dialog, feedback); | ||
const result = await handleFeedbackSubmit(dialog, feedback, { referrer: options.referrer }); | ||
@@ -109,3 +109,3 @@ // Error submitting feedback | ||
function removeActor(): void { | ||
actor && actor.$el.remove(); | ||
actor && actor.el.remove(); | ||
} | ||
@@ -146,3 +146,3 @@ | ||
shadow.appendChild(dialog.$el); | ||
shadow.appendChild(dialog.el); | ||
@@ -181,3 +181,3 @@ // Hides the default actor whenever dialog is opened | ||
hideDialog(); | ||
dialog.$el.remove(); | ||
dialog.el.remove(); | ||
dialog = undefined; | ||
@@ -206,3 +206,3 @@ } | ||
actor = Actor({ options, onClick: handleActorClick }); | ||
shadow.appendChild(actor.$el); | ||
shadow.appendChild(actor.el); | ||
} else { | ||
@@ -209,0 +209,0 @@ attachTo.addEventListener('click', handleActorClick); |
import type { FeedbackComponent, FeedbackConfigurationWithDefaults, FeedbackFormData } from '../types'; | ||
import { Form } from './Form'; | ||
import { createElement as h } from './util/createElement'; | ||
import { createElement } from './util/createElement'; | ||
@@ -62,3 +62,3 @@ interface DialogProps { | ||
}: DialogProps): DialogComponent { | ||
let $el: HTMLDialogElement | null = null; | ||
let el: HTMLDialogElement | null = null; | ||
@@ -82,4 +82,4 @@ /** | ||
function close(): void { | ||
if ($el) { | ||
$el.open = false; | ||
if (el) { | ||
el.open = false; | ||
} | ||
@@ -92,4 +92,4 @@ } | ||
function open(): void { | ||
if ($el) { | ||
$el.open = true; | ||
if (el) { | ||
el.open = true; | ||
} | ||
@@ -102,7 +102,7 @@ } | ||
function checkIsOpen(): boolean { | ||
return ($el && $el.open === true) || false; | ||
return (el && el.open === true) || false; | ||
} | ||
const { | ||
$el: $form, | ||
el: formEl, | ||
setSubmitEnabled, | ||
@@ -120,3 +120,3 @@ setSubmitDisabled, | ||
$el = h( | ||
el = createElement( | ||
'dialog', | ||
@@ -128,3 +128,3 @@ { | ||
}, | ||
h( | ||
createElement( | ||
'div', | ||
@@ -138,4 +138,4 @@ { | ||
}, | ||
h('h2', { className: 'dialog__header' }, options.formTitle), | ||
$form, | ||
createElement('h2', { className: 'dialog__header' }, options.formTitle), | ||
formEl, | ||
), | ||
@@ -145,3 +145,3 @@ ); | ||
return { | ||
$el, | ||
el, | ||
showError, | ||
@@ -148,0 +148,0 @@ hideError, |
import type { FeedbackComponent, FeedbackConfigurationWithDefaults, FeedbackFormData } from '../types'; | ||
import { SubmitButton } from './SubmitButton'; | ||
import { createElement as h } from './util/createElement'; | ||
import { createElement } from './util/createElement'; | ||
@@ -74,3 +74,3 @@ interface Props { | ||
const { | ||
$el: $submit, | ||
el: submitEl, | ||
setDisabled: setSubmitDisabled, | ||
@@ -105,3 +105,3 @@ setEnabled: setSubmitEnabled, | ||
const $error = h('div', { | ||
const errorEl = createElement('div', { | ||
className: 'form__error-container form__error-container--hidden', | ||
@@ -112,14 +112,14 @@ ariaHidden: 'true', | ||
function showError(message: string): void { | ||
$error.textContent = message; | ||
$error.classList.remove('form__error-container--hidden'); | ||
$error.setAttribute('ariaHidden', 'false'); | ||
errorEl.textContent = message; | ||
errorEl.classList.remove('form__error-container--hidden'); | ||
errorEl.setAttribute('ariaHidden', 'false'); | ||
} | ||
function hideError(): void { | ||
$error.textContent = ''; | ||
$error.classList.add('form__error-container--hidden'); | ||
$error.setAttribute('ariaHidden', 'true'); | ||
errorEl.textContent = ''; | ||
errorEl.classList.add('form__error-container--hidden'); | ||
errorEl.setAttribute('ariaHidden', 'true'); | ||
} | ||
const $name = h('input', { | ||
const nameEl = createElement('input', { | ||
id: 'name', | ||
@@ -134,3 +134,3 @@ type: showName ? 'text' : 'hidden', | ||
const $email = h('input', { | ||
const emailEl = createElement('input', { | ||
id: 'email', | ||
@@ -145,3 +145,3 @@ type: showEmail ? 'text' : 'hidden', | ||
const $message = h('textarea', { | ||
const messageEl = createElement('textarea', { | ||
id: 'message', | ||
@@ -166,3 +166,3 @@ autoFocus: 'true', | ||
const $cancel = h( | ||
const cancelEl = createElement( | ||
'button', | ||
@@ -179,3 +179,3 @@ { | ||
const $form = h( | ||
const formEl = createElement( | ||
'form', | ||
@@ -187,7 +187,7 @@ { | ||
[ | ||
$error, | ||
errorEl, | ||
!isAnonymous && | ||
showName && | ||
h( | ||
createElement( | ||
'label', | ||
@@ -198,9 +198,9 @@ { | ||
}, | ||
[nameLabel, $name], | ||
[nameLabel, nameEl], | ||
), | ||
!isAnonymous && !showName && $name, | ||
!isAnonymous && !showName && nameEl, | ||
!isAnonymous && | ||
showEmail && | ||
h( | ||
createElement( | ||
'label', | ||
@@ -211,7 +211,7 @@ { | ||
}, | ||
[emailLabel, $email], | ||
[emailLabel, emailEl], | ||
), | ||
!isAnonymous && !showEmail && $email, | ||
!isAnonymous && !showEmail && emailEl, | ||
h( | ||
createElement( | ||
'label', | ||
@@ -222,6 +222,6 @@ { | ||
}, | ||
[messageLabel, $message], | ||
[messageLabel, messageEl], | ||
), | ||
h( | ||
createElement( | ||
'div', | ||
@@ -231,3 +231,3 @@ { | ||
}, | ||
[$submit, $cancel], | ||
[submitEl, cancelEl], | ||
), | ||
@@ -238,3 +238,3 @@ ], | ||
return { | ||
$el: $form, | ||
el: formEl, | ||
setSubmitDisabled, | ||
@@ -241,0 +241,0 @@ setSubmitEnabled, |
@@ -0,1 +1,3 @@ | ||
import { WINDOW } from '@sentry/browser'; | ||
import { setAttributesNS } from '../util/setAttributesNS'; | ||
@@ -7,3 +9,3 @@ | ||
interface IconReturn { | ||
$el: SVGElement; | ||
el: SVGElement; | ||
} | ||
@@ -15,6 +17,5 @@ | ||
export function Icon(): IconReturn { | ||
const cENS = <K extends keyof SVGElementTagNameMap>(tagName: K): SVGElementTagNameMap[K] => | ||
// eslint-disable-next-line no-restricted-globals | ||
document.createElementNS(XMLNS, tagName); | ||
const svg = setAttributesNS(cENS('svg'), { | ||
const createElementNS = <K extends keyof SVGElementTagNameMap>(tagName: K): SVGElementTagNameMap[K] => | ||
WINDOW.document.createElementNS(XMLNS, tagName); | ||
const svg = setAttributesNS(createElementNS('svg'), { | ||
class: 'feedback-icon', | ||
@@ -27,7 +28,7 @@ width: `${SIZE}`, | ||
const g = setAttributesNS(cENS('g'), { | ||
const g = setAttributesNS(createElementNS('g'), { | ||
clipPath: 'url(#clip0_57_80)', | ||
}); | ||
const path = setAttributesNS(cENS('path'), { | ||
const path = setAttributesNS(createElementNS('path'), { | ||
['fill-rule']: 'evenodd', | ||
@@ -39,8 +40,8 @@ ['clip-rule']: 'evenodd', | ||
const speakerDefs = cENS('defs'); | ||
const speakerClipPathDef = setAttributesNS(cENS('clipPath'), { | ||
const speakerDefs = createElementNS('defs'); | ||
const speakerClipPathDef = setAttributesNS(createElementNS('clipPath'), { | ||
id: 'clip0_57_80', | ||
}); | ||
const speakerRect = setAttributesNS(cENS('rect'), { | ||
const speakerRect = setAttributesNS(createElementNS('rect'), { | ||
width: `${SIZE}`, | ||
@@ -57,4 +58,4 @@ height: `${SIZE}`, | ||
return { | ||
$el: svg, | ||
el: svg, | ||
}; | ||
} |
import type { FeedbackComponent } from '../types'; | ||
import { createElement as h } from './util/createElement'; | ||
import { createElement } from './util/createElement'; | ||
@@ -24,3 +24,3 @@ interface SubmitButtonProps { | ||
export function SubmitButton({ label }: SubmitButtonProps): SubmitButtonComponent { | ||
const $el = h( | ||
const el = createElement( | ||
'button', | ||
@@ -37,13 +37,13 @@ { | ||
return { | ||
$el, | ||
el, | ||
setDisabled: () => { | ||
$el.disabled = true; | ||
$el.ariaDisabled = 'disabled'; | ||
el.disabled = true; | ||
el.ariaDisabled = 'disabled'; | ||
}, | ||
setEnabled: () => { | ||
$el.disabled = false; | ||
$el.ariaDisabled = 'false'; | ||
$el.removeAttribute('ariaDisabled'); | ||
el.disabled = false; | ||
el.ariaDisabled = 'false'; | ||
el.removeAttribute('ariaDisabled'); | ||
}, | ||
}; | ||
} |
@@ -0,1 +1,3 @@ | ||
import { WINDOW } from '@sentry/browser'; | ||
import { setAttributesNS } from '../util/setAttributesNS'; | ||
@@ -8,3 +10,3 @@ | ||
interface IconReturn { | ||
$el: SVGElement; | ||
el: SVGElement; | ||
} | ||
@@ -16,6 +18,5 @@ | ||
export function SuccessIcon(): IconReturn { | ||
const cENS = <K extends keyof SVGElementTagNameMap>(tagName: K): SVGElementTagNameMap[K] => | ||
// eslint-disable-next-line no-restricted-globals | ||
document.createElementNS(XMLNS, tagName); | ||
const svg = setAttributesNS(cENS('svg'), { | ||
const createElementNS = <K extends keyof SVGElementTagNameMap>(tagName: K): SVGElementTagNameMap[K] => | ||
WINDOW.document.createElementNS(XMLNS, tagName); | ||
const svg = setAttributesNS(createElementNS('svg'), { | ||
class: 'success-icon', | ||
@@ -28,7 +29,7 @@ width: `${WIDTH}`, | ||
const g = setAttributesNS(cENS('g'), { | ||
const g = setAttributesNS(createElementNS('g'), { | ||
clipPath: 'url(#clip0_57_156)', | ||
}); | ||
const path2 = setAttributesNS(cENS('path'), { | ||
const path2 = setAttributesNS(createElementNS('path'), { | ||
['fill-rule']: 'evenodd', | ||
@@ -38,3 +39,3 @@ ['clip-rule']: 'evenodd', | ||
}); | ||
const path = setAttributesNS(cENS('path'), { | ||
const path = setAttributesNS(createElementNS('path'), { | ||
d: 'M6.68775 12.4297C6.78586 12.4745 6.89218 12.4984 7 12.5C7.11275 12.4955 7.22315 12.4664 7.32337 12.4145C7.4236 12.3627 7.51121 12.2894 7.58 12.2L12 5.63999C12.0848 5.47724 12.1071 5.28902 12.0625 5.11098C12.0178 4.93294 11.9095 4.77744 11.7579 4.67392C11.6064 4.57041 11.4221 4.52608 11.24 4.54931C11.0579 4.57254 10.8907 4.66173 10.77 4.79999L6.88 10.57L5.13 8.56999C5.06508 8.49566 4.98613 8.43488 4.89768 8.39111C4.80922 8.34735 4.713 8.32148 4.61453 8.31498C4.51605 8.30847 4.41727 8.32147 4.32382 8.35322C4.23038 8.38497 4.14413 8.43484 4.07 8.49999C3.92511 8.63217 3.83692 8.81523 3.82387 9.01092C3.81083 9.2066 3.87393 9.39976 4 9.54999L6.43 12.24C6.50187 12.3204 6.58964 12.385 6.68775 12.4297Z', | ||
@@ -45,8 +46,8 @@ }); | ||
const speakerDefs = cENS('defs'); | ||
const speakerClipPathDef = setAttributesNS(cENS('clipPath'), { | ||
const speakerDefs = createElementNS('defs'); | ||
const speakerClipPathDef = setAttributesNS(createElementNS('clipPath'), { | ||
id: 'clip0_57_156', | ||
}); | ||
const speakerRect = setAttributesNS(cENS('rect'), { | ||
const speakerRect = setAttributesNS(createElementNS('rect'), { | ||
width: `${WIDTH}`, | ||
@@ -64,4 +65,4 @@ height: `${WIDTH}`, | ||
return { | ||
$el: svg, | ||
el: svg, | ||
}; | ||
} |
import type { FeedbackComponent } from '../types'; | ||
import { SuccessIcon } from './SuccessIcon'; | ||
import { createElement as h } from './util/createElement'; | ||
import { createElement } from './util/createElement'; | ||
@@ -22,11 +22,11 @@ interface SuccessMessageProps { | ||
function remove(): void { | ||
if (!$el) { | ||
if (!el) { | ||
return; | ||
} | ||
$el.remove(); | ||
el.remove(); | ||
onRemove && onRemove(); | ||
} | ||
const $el = h( | ||
const el = createElement( | ||
'div', | ||
@@ -37,3 +37,3 @@ { | ||
}, | ||
SuccessIcon().$el, | ||
SuccessIcon().el, | ||
message, | ||
@@ -43,5 +43,5 @@ ); | ||
return { | ||
$el, | ||
el, | ||
remove, | ||
}; | ||
} |
@@ -0,1 +1,3 @@ | ||
import { WINDOW } from '@sentry/browser'; | ||
/** | ||
@@ -10,4 +12,4 @@ * Helper function to create an element. Could be used as a JSX factory | ||
): HTMLElementTagNameMap[K] { | ||
// eslint-disable-next-line no-restricted-globals | ||
const element = document.createElement(tagName); | ||
const doc = WINDOW.document; | ||
const element = doc.createElement(tagName); | ||
@@ -36,2 +38,3 @@ if (attributes) { | ||
function appendChild(parent: Node, child: any): void { | ||
const doc = WINDOW.document; | ||
if (typeof child === 'undefined' || child === null) { | ||
@@ -48,10 +51,8 @@ return; | ||
} 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))); | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
361291
6123
205
+ Added@sentry-internal/tracing@7.75.1(transitive)
+ Added@sentry/browser@7.75.1(transitive)
+ Added@sentry/core@7.75.1(transitive)
+ Added@sentry/replay@7.75.1(transitive)
+ Added@sentry/types@7.75.1(transitive)
+ Added@sentry/utils@7.75.1(transitive)
- Removed@sentry-internal/tracing@7.75.0(transitive)
- Removed@sentry/browser@7.75.0(transitive)
- Removed@sentry/core@7.75.0(transitive)
- Removed@sentry/replay@7.75.0(transitive)
- Removed@sentry/types@7.75.0(transitive)
- Removed@sentry/utils@7.75.0(transitive)
Updated@sentry/browser@7.75.1
Updated@sentry/core@7.75.1
Updated@sentry/types@7.75.1
Updated@sentry/utils@7.75.1