Skip to content

Commit 1fbd163

Browse files
committed
try/catch all the things, fix dialog not opening, add autoInject option
1 parent ac369b7 commit 1fbd163

File tree

4 files changed

+184
-121
lines changed

4 files changed

+184
-121
lines changed

packages/feedback/src/index.ts

Lines changed: 138 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { getCurrentHub } from '@sentry/core';
22
import type { Integration } from '@sentry/types';
33
import { isNodeEnv, logger } from '@sentry/utils';
44

5-
import { sendFeedback } from './sendFeedback';
65
import type { FeedbackConfigurationWithDefaults, FeedbackFormData } from './types';
6+
import { handleFeedbackSubmit } from './util/handleFeedbackSubmit';
77
import { sendFeedbackRequest } from './util/sendFeedbackRequest';
88
import { Actor } from './widget/Actor';
99
import { createActorStyles } from './widget/Actor.css';
@@ -43,7 +43,9 @@ const THEME = {
4343
};
4444

4545
/**
46-
*
46+
* Feedback integration. When added as an integration to the SDK, it will
47+
* inject a button in the bottom-right corner of the window that opens a
48+
* feedback modal when clicked.
4749
*/
4850
export class Feedback implements Integration {
4951
/**
@@ -89,6 +91,7 @@ export class Feedback implements Integration {
8991

9092
public constructor({
9193
attachTo = null,
94+
autoInject = true,
9295
showEmail = true,
9396
showName = true,
9497
useSentryUser = {
@@ -124,6 +127,7 @@ export class Feedback implements Integration {
124127

125128
this.options = {
126129
attachTo,
130+
autoInject,
127131
isAnonymous,
128132
isEmailRequired,
129133
isNameRequired,
@@ -159,7 +163,48 @@ export class Feedback implements Integration {
159163
return;
160164
}
161165

162-
this._injectWidget();
166+
try {
167+
// TODO: This is only here for hot reloading
168+
if (this._host) {
169+
this.remove();
170+
}
171+
const existingFeedback = document.querySelector('#sentry-feedback');
172+
if (existingFeedback) {
173+
existingFeedback.remove();
174+
}
175+
176+
// TODO: End hotloading
177+
178+
try {
179+
this._shadow = this._createShadowHost();
180+
} catch (err) {
181+
return;
182+
}
183+
184+
// Only create widget actor if `attachTo` was not defined
185+
if (this.options.attachTo) {
186+
const actorTarget = typeof this.options.attachTo === 'string' ? document.querySelector(this.options.attachTo) : typeof this.options.attachTo === 'function' ? this.options.attachTo : null;
187+
188+
if (!actorTarget) {
189+
logger.warn(`[Feedback] Unable to find element with selector ${actorTarget}`);
190+
return;
191+
}
192+
193+
actorTarget.addEventListener('click', this._handleActorClick);
194+
} else if (this.options.autoInject) {
195+
// Only
196+
this._createWidgetActor();
197+
}
198+
199+
if (!this._host) {
200+
return;
201+
}
202+
203+
document.body.appendChild(this._host);
204+
} catch (err) {
205+
// TODO: error handling
206+
console.error(err);
207+
}
163208
}
164209

165210
/**
@@ -175,43 +220,55 @@ export class Feedback implements Integration {
175220
* Opens the Feedback dialog form
176221
*/
177222
public openDialog(): void {
178-
if (this._dialog) {
179-
this._dialog.open();
180-
return;
181-
}
223+
try {
224+
if (this._dialog) {
225+
this._dialog.open();
226+
this._isDialogOpen = true;
227+
console.log('dialog already open')
228+
return;
229+
}
182230

183-
if (!this._shadow) {
184-
this._shadow = this._createShadowHost();
185-
}
231+
try {
232+
this._shadow = this._createShadowHost();
233+
} catch {
234+
return;
235+
}
186236

187-
// Lazy-load until dialog is opened and only inject styles once
188-
if (!this._hasDialogOpened) {
189-
this._shadow.appendChild(createDialogStyles(document, THEME));
190-
}
237+
console.log('open dialog', this._shadow)
238+
// Lazy-load until dialog is opened and only inject styles once
239+
if (!this._hasDialogOpened) {
240+
this._shadow.appendChild(createDialogStyles(document, THEME));
241+
}
191242

192-
const userKey = this.options.useSentryUser;
193-
const scope = getCurrentHub().getScope();
194-
const user = scope && scope.getUser();
195-
196-
this._dialog = Dialog({
197-
defaultName: (userKey && user && user[userKey.name]) || '',
198-
defaultEmail: (userKey && user && user[userKey.email]) || '',
199-
onClose: () => {
200-
this.showActor();
201-
},
202-
onCancel: () => {
203-
this.hideDialog();
204-
this.showActor();
205-
},
206-
onSubmit: this._handleFeedbackSubmit,
207-
options: this.options,
208-
});
209-
this._shadow.appendChild(this._dialog.$el);
210-
211-
// Hides the default actor whenever dialog is opened
212-
this._actor && this._actor.hide();
213-
214-
this._hasDialogOpened = true;
243+
const userKey = this.options.useSentryUser;
244+
const scope = getCurrentHub().getScope();
245+
const user = scope && scope.getUser();
246+
247+
this._dialog = Dialog({
248+
defaultName: (userKey && user && user[userKey.name]) || '',
249+
defaultEmail: (userKey && user && user[userKey.email]) || '',
250+
onClose: () => {
251+
this.showActor();
252+
this._isDialogOpen = false;
253+
},
254+
onCancel: () => {
255+
this.hideDialog();
256+
this.showActor();
257+
},
258+
onSubmit: this._handleFeedbackSubmit,
259+
options: this.options,
260+
});
261+
this._shadow.appendChild(this._dialog.$el);
262+
console.log(this._dialog.$el);
263+
264+
// Hides the default actor whenever dialog is opened
265+
this._actor && this._actor.hide();
266+
267+
this._hasDialogOpened = true;
268+
} catch (err) {
269+
// TODO: Error handling?
270+
console.error(err);
271+
}
215272
}
216273

217274
/**
@@ -220,6 +277,7 @@ export class Feedback implements Integration {
220277
public hideDialog = (): void => {
221278
if (this._dialog) {
222279
this._dialog.close();
280+
this._isDialogOpen = false;
223281
}
224282
};
225283

@@ -244,57 +302,27 @@ export class Feedback implements Integration {
244302
};
245303

246304
/**
247-
*
305+
* Creates the host element of widget's shadow DOM. Returns null if not supported.
248306
*/
249-
protected _injectWidget(): void {
250-
// TODO: This is only here for hot reloading
251-
if (this._host) {
252-
this.remove();
253-
}
254-
const existingFeedback = document.querySelector('#sentry-feedback');
255-
if (existingFeedback) {
256-
existingFeedback.remove();
257-
}
258-
259-
// TODO: End hotloading
260-
261-
this._shadow = this._createShadowHost();
262-
263-
// Only create widget actor if `attachTo` was not defined
264-
if (this.options.attachTo === null) {
265-
this._createWidgetActor();
266-
} else {
267-
const actorTarget = document.querySelector(this.options.attachTo);
268-
269-
if (!actorTarget) {
270-
logger.warn(`[Feedback] Unable to find element with selector ${actorTarget}`);
271-
return;
272-
}
273-
274-
actorTarget.addEventListener('click', this._handleActorClick);
307+
protected _createShadowHost(): ShadowRoot {
308+
if (!document.head.attachShadow) {
309+
// Shadow DOM not supported
310+
logger.warn('[Feedback] Browser does not support shadow DOM API')
311+
throw new Error('Browser does not support shadow DOM API.')
275312
}
276313

277-
if (!this._host) {
278-
return;
314+
// Don't create if it already exists
315+
if (this._shadow) {
316+
return this._shadow;
279317
}
280318

281-
document.body.appendChild(this._host);
282-
}
283-
284-
/**
285-
* Creates the host element of widget's shadow DOM
286-
*/
287-
protected _createShadowHost(): ShadowRoot {
288319
// Create the host
289320
this._host = document.createElement('div');
290321
this._host.id = 'sentry-feedback';
291322

292323
// Create the shadow root
293324
const shadow = this._host.attachShadow({ mode: 'open' });
294325

295-
// Insert styles for actor
296-
shadow.appendChild(createActorStyles(document, THEME));
297-
298326
return shadow;
299327
}
300328

@@ -307,12 +335,17 @@ export class Feedback implements Integration {
307335
return;
308336
}
309337

310-
this._shadow.appendChild(createActorStyles(document, THEME));
338+
try {
339+
this._shadow.appendChild(createActorStyles(document, THEME));
311340

312-
// Create Actor component
313-
this._actor = Actor({ options: this.options, theme: THEME, onClick: this._handleActorClick });
341+
// Create Actor component
342+
this._actor = Actor({ options: this.options, theme: THEME, onClick: this._handleActorClick });
314343

315-
this._shadow.appendChild(this._actor.$el);
344+
this._shadow.appendChild(this._actor.$el);
345+
} catch(err) {
346+
// TODO: error handling
347+
console.error(err);
348+
}
316349
}
317350

318351
/**
@@ -323,24 +356,29 @@ export class Feedback implements Integration {
323356
return;
324357
}
325358

326-
const success = SuccessMessage({
327-
message: this.options.successMessageText,
328-
onRemove: () => {
329-
if (timeoutId) {
330-
clearTimeout(timeoutId);
359+
try {
360+
const success = SuccessMessage({
361+
message: this.options.successMessageText,
362+
onRemove: () => {
363+
if (timeoutId) {
364+
clearTimeout(timeoutId);
365+
}
366+
this.showActor();
367+
},
368+
theme: THEME,
369+
});
370+
371+
this._shadow.appendChild(success.$el);
372+
373+
const timeoutId = setTimeout(() => {
374+
if (success) {
375+
success.remove();
331376
}
332-
this.showActor();
333-
},
334-
theme: THEME,
335-
});
336-
337-
this._shadow.appendChild(success.$el);
338-
339-
const timeoutId = setTimeout(() => {
340-
if (success) {
341-
success.remove();
342-
}
343-
}, 5000);
377+
}, 5000);
378+
} catch(err) {
379+
// TODO: error handling
380+
console.error(err);
381+
}
344382
}
345383

346384
/**
@@ -368,31 +406,12 @@ export class Feedback implements Integration {
368406
* create and send the feedback message as an event.
369407
*/
370408
protected _handleFeedbackSubmit = async (feedback: FeedbackFormData): Promise<void> => {
371-
console.log('ahndle feedback submit');
372-
if (!this._dialog) {
373-
// Not sure when this would happen
374-
return;
375-
}
409+
const result = await handleFeedbackSubmit(this._dialog, feedback);
376410

377-
try {
378-
this._dialog.hideError();
379-
this._dialog.setSubmitDisabled();
380-
const resp = await sendFeedback(feedback);
381-
console.log({ resp });
382-
if (resp) {
383-
// Success!
411+
// Success
412+
if (result) {
384413
this.removeDialog();
385414
this._showSuccessMessage();
386-
return;
387-
}
388-
389-
// Errored... re-enable submit button
390-
this._dialog.setSubmitEnabled();
391-
this._dialog.showError('There was a problem submitting feedback, please wait and try again.');
392-
} catch {
393-
// Errored... re-enable submit button
394-
this._dialog.setSubmitEnabled();
395-
this._dialog.showError('There was a problem submitting feedback, please wait and try again.');
396415
}
397416
};
398417
}

packages/feedback/src/types/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,13 @@ export interface FeedbackConfigurationWithDefaults {
3838
/**
3939
* DOM Selector to attach click listener to, for opening Feedback dialog.
4040
*/
41-
attachTo: string | null;
41+
attachTo: Node | string | null;
42+
43+
/**
44+
* Auto-inject default Feedback actor button to the DOM when integration is
45+
* added.
46+
*/
47+
autoInject: boolean;
4248

4349
/**
4450
* If true, will not collect user data (email/name).

0 commit comments

Comments
 (0)