Skip to content

Commit

Permalink
Merge pull request #1067 from ga-devfront/feat/add-front-sentry-report
Browse files Browse the repository at this point in the history
[NEW UI] Update page - sentry user feedback
  • Loading branch information
Quetzacoalt91 authored Dec 11, 2024
2 parents 40f932c + 52c7622 commit c4a358f
Show file tree
Hide file tree
Showing 24 changed files with 472 additions and 39 deletions.
1 change: 1 addition & 0 deletions _dev/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface AutoUpgradeVariables {
admin_url: string;
admin_dir: string;
stepper_parent_id: string;
module_version: string;
}

declare global {
Expand Down
3 changes: 2 additions & 1 deletion _dev/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ window.AutoUpgradeVariables = {
token: 'test-token',
admin_url: 'http://localhost',
admin_dir: '/admin_directory',
stepper_parent_id: 'stepper_content'
stepper_parent_id: 'stepper_content',
module_version: '7.1.0'
};

beforeAll(() => {});
86 changes: 86 additions & 0 deletions _dev/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions _dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"vite-plugin-static-copy": "^1.0.6"
},
"dependencies": {
"@sentry/browser": "^8.41.0",
"axios": "^1.7.7"
}
}
96 changes: 96 additions & 0 deletions _dev/src/ts/api/sentryApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as Sentry from '@sentry/browser';
import { SeverityLevel } from '@sentry/browser';
import { maskSensitiveInfoInUrl } from '../utils/urlUtils';
import { Feedback, Logs, LogsFields } from '../types/sentryApi';

const adminDir = window.AutoUpgradeVariables.admin_dir;
const feedbackModalTag = 'feedbackModal';

Sentry.init({
dsn: 'https://[email protected]/4507254110552064',
release: `v${window.AutoUpgradeVariables.module_version}`,
sendDefaultPii: false,
beforeSend(event) {
if (event.tags?.source !== feedbackModalTag) {
return null;
}

if (event.request?.url) {
event.request.url = maskSensitiveInfoInUrl(window.location.href, adminDir);
}

return event;
},
beforeBreadcrumb(breadcrumb) {
['url', 'from', 'to'].forEach((key) => {
if (breadcrumb.data?.[key]) {
breadcrumb.data[key] = maskSensitiveInfoInUrl(breadcrumb.data[key], adminDir);
}
});

return breadcrumb;
}
});

/**
* Sends enriched user feedback to Sentry with optional logs and metadata.
* This function attaches log files, captures a custom event, and optionally sends user feedback with an associated event ID.
*
* @param {string} message - The message to describe the feedback or error.
* @param {Logs} [logs={}] - An object containing optional logs, warnings, and errors to attach.
* @param {Feedback} [feedback={}] - An object containing optional user feedback fields such as email and comments.
* @param {SeverityLevel} [level='error'] - The severity level of the event (e.g., 'info', 'warning', 'error').
*/
export function sendUserFeedback(
message: string,
logs: Logs = {},
feedback: Feedback = {},
level: SeverityLevel = 'error'
) {
const attachments: { key: LogsFields; filename: string }[] = [
{ key: LogsFields.LOGS, filename: 'logs.txt' },
{ key: LogsFields.WARNINGS, filename: 'summary_warnings.txt' },
{ key: LogsFields.ERRORS, filename: 'summary_errors.txt' }
];

attachments.forEach(({ key, filename }) => {
if (logs[key]) {
Sentry.getCurrentScope().addAttachment({
filename,
data: logs[key],
contentType: 'text/plain'
});
}
});

const maskedUrl = maskSensitiveInfoInUrl(window.location.href, adminDir);

const eventId = Sentry.captureEvent({
message,
level,
tags: {
url: maskedUrl,
source: feedbackModalTag
}
});

if (feedback.email || feedback.comments) {
Sentry.captureFeedback(
{
associatedEventId: eventId,
email: feedback.email,
message: feedback.comments ?? ''
},
{
captureContext: {
tags: {
url: maskedUrl,
source: feedbackModalTag
}
}
}
);
}

Sentry.getCurrentScope().clearAttachments();
}
3 changes: 3 additions & 0 deletions _dev/src/ts/components/LogsViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ export default class LogsViewer extends ComponentAbstract implements Destroyable
*/
#createSummary(severity: SeverityClasses, logs: string[]): HTMLDivElement {
const summaryFragment = this.#templateSummary.content.cloneNode(true) as DocumentFragment;

const summary = summaryFragment.querySelector('.logs__summary') as HTMLDivElement;
summary.setAttribute('data-summary-severity', severity);

const summaryScroll = summaryFragment.querySelector('.logs__summary-scroll') as HTMLDivElement;

const title = this.#getSummaryTitle(severity);
Expand Down
75 changes: 75 additions & 0 deletions _dev/src/ts/dialogs/SendErrorReportDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import DomLifecycle from '../types/DomLifecycle';
import { sendUserFeedback } from '../api/sentryApi';
import { Feedback, FeedbackFields, Logs } from '../types/sentryApi';

export default class SendErrorReportDialog implements DomLifecycle {
protected readonly formId = 'form-error-feedback';

public mount = (): void => {
this.#form.addEventListener('submit', this.#onSubmit);
};

public beforeDestroy = (): void => {
this.#form.removeEventListener('submit', this.#onSubmit);
};

get #form(): HTMLFormElement {
const form = document.forms.namedItem(this.formId);
if (!form) {
throw new Error('Form not found');
}

return form;
}

#onSubmit = (event: SubmitEvent) => {
event.preventDefault();

const logsViewer = document.querySelector('[data-component="logs-viewer"]');

const logs: Logs = {};

const logsContent = logsViewer?.querySelector('[data-slot-component="list"]');
if (!logsContent) {
throw new Error('Logs content to send not found');
}

const message = logsContent.lastChild?.textContent;
if (!message) {
throw new Error('Message to send not found');
}

if (!logsContent.textContent) {
throw new Error('Logs to send not found');
}
logs.logs = logsContent.textContent;

const summaryWarningText = logsViewer?.querySelector(
'[data-summary-severity="warning"]'
)?.textContent;
if (summaryWarningText) {
logs.warnings = summaryWarningText;
}

const summaryErrorText = logsViewer?.querySelector(
'[data-summary-severity="error"]'
)?.textContent;
if (summaryErrorText) {
logs.errors = summaryErrorText;
}

const feedback: Feedback = {};

const form = event.target as HTMLFormElement;
const formData = new FormData(form);

Object.values(FeedbackFields).forEach((field) => {
const value = formData.get(field);
if (value && typeof value === 'string') {
feedback[field] = value;
}
});

sendUserFeedback(message, logs, feedback);
};
}
2 changes: 1 addition & 1 deletion _dev/src/ts/dialogs/StartUpdateDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default class StartUpdateDialog implements DomLifecycle {
}

get #form(): HTMLFormElement {
const form = document.forms.namedItem('form-confirm-update');
const form = document.forms.namedItem(this.formId);
if (!form) {
throw new Error('Form not found');
}
Expand Down
15 changes: 10 additions & 5 deletions _dev/src/ts/pages/UpdatePageUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class UpdatePageUpdate extends UpdatePage {
#progressTracker: ProgressTracker = new ProgressTracker(this.#progressTrackerContainer);
#restoreAlertForm: null | HTMLFormElement = null;
#restoreButtonForm: null | HTMLFormElement = null;
#submitErrorReportForm: null | HTMLFormElement = null;

constructor() {
super();
Expand All @@ -32,8 +33,9 @@ export default class UpdatePageUpdate extends UpdatePage {
public beforeDestroy = () => {
this.#progressTracker.beforeDestroy();

this.#restoreAlertForm?.removeEventListener('submit', this.#handleRestoreSubmit);
this.#restoreButtonForm?.removeEventListener('submit', this.#handleRestoreSubmit);
this.#restoreAlertForm?.removeEventListener('submit', this.#handleSubmit);
this.#restoreButtonForm?.removeEventListener('submit', this.#handleSubmit);
this.#submitErrorReportForm?.removeEventListener('submit', this.#handleSubmit);
};

get #progressTrackerContainer(): HTMLDivElement {
Expand Down Expand Up @@ -77,7 +79,7 @@ export default class UpdatePageUpdate extends UpdatePage {
alertContainer.classList.remove('hidden');

this.#restoreAlertForm = document.forms.namedItem('restore-alert');
this.#restoreAlertForm?.addEventListener('submit', this.#handleRestoreSubmit);
this.#restoreAlertForm?.addEventListener('submit', this.#handleSubmit);
};

#displayErrorButtons = () => {
Expand All @@ -89,11 +91,14 @@ export default class UpdatePageUpdate extends UpdatePage {

buttonsContainer.classList.remove('hidden');

this.#submitErrorReportForm = document.forms.namedItem('submit-error-report');
this.#submitErrorReportForm?.addEventListener('submit', this.#handleSubmit);

this.#restoreButtonForm = document.forms.namedItem('restore-button');
this.#restoreButtonForm?.addEventListener('submit', this.#handleRestoreSubmit);
this.#restoreButtonForm?.addEventListener('submit', this.#handleSubmit);
};

#handleRestoreSubmit = async (event: SubmitEvent) => {
#handleSubmit = async (event: SubmitEvent) => {
event.preventDefault();

const form = event.target as HTMLFormElement;
Expand Down
10 changes: 7 additions & 3 deletions _dev/src/ts/routing/ScriptHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import UpdatePageUpdateOptions from '../pages/UpdatePageUpdateOptions';
import UpdatePageBackup from '../pages/UpdatePageBackup';
import UpdatePageUpdate from '../pages/UpdatePageUpdate';
import UpdatePagePostUpdate from '../pages/UpdatePagePostUpdate';

import StartUpdateDialog from '../dialogs/StartUpdateDialog';
import SendErrorReportDialog from '../dialogs/SendErrorReportDialog';

import DomLifecycle from '../types/DomLifecycle';
import { RoutesMatching } from '../types/scriptHandlerTypes';
import { routeHandler } from '../autoUpgrade';
import StartUpdateDialog from '../dialogs/StartUpdateDialog';

export default class ScriptHandler {
#currentScript: DomLifecycle | undefined;
Expand All @@ -25,7 +28,8 @@ export default class ScriptHandler {
'update-page-update': UpdatePageUpdate,
'update-page-post-update': UpdatePagePostUpdate,

'start-update-dialog': StartUpdateDialog
'start-update-dialog': StartUpdateDialog,
'send-error-report-dialog': SendErrorReportDialog
};

/**
Expand All @@ -42,7 +46,7 @@ export default class ScriptHandler {

/**
* @public
* @param {string} routeName - The name of the route to load his associated script.
* @param {string} scriptID - The ID of the route to load his associated script.
* @returns void
* @description Loads and mounts the page script associated with the specified route name.
*/
Expand Down
Loading

0 comments on commit c4a358f

Please sign in to comment.