tycho-components 0.42.3 → 0.42.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,7 +44,7 @@ export const CommonTexts = {
44
44
  'internal.server.error': 'An unexpected error occurred. Contact the administrator.',
45
45
  'server.error.report.prompt': 'Something went wrong on our end. Click here to tell us what happened.',
46
46
  'server.error.report.modal.title': 'Report a problem',
47
- 'server.error.report.modal.subtitle': 'Describe what you were doing when the error occurred. We will use your message to investigate.',
47
+ 'server.error.report.modal.subtitle': 'Describe what you were doing when the error occurred. We will also include recent console output and a screenshot of this page.',
48
48
  'server.error.report.modal.placeholder': 'What were you trying to do? Any extra details help us reproduce the issue.',
49
49
  'server.error.report.modal.send': 'Send report',
50
50
  'server.error.report.validation': 'Please describe what happened before sending.',
@@ -132,7 +132,7 @@ export const CommonTexts = {
132
132
  'internal.server.error': 'Ocorreu um erro inesperado. Entre em contato com o administrador.',
133
133
  'server.error.report.prompt': 'Algo deu errado do nosso lado. Clique aqui para nos contar o que aconteceu.',
134
134
  'server.error.report.modal.title': 'Reportar um problema',
135
- 'server.error.report.modal.subtitle': 'Descreva o que você estava fazendo quando o erro ocorreu. Usaremos sua mensagem para investigar.',
135
+ 'server.error.report.modal.subtitle': 'Descreva o que você estava fazendo quando o erro ocorreu. Também incluiremos a saída recente do console e uma captura de tela desta página.',
136
136
  'server.error.report.modal.placeholder': 'O que você estava tentando fazer? Detalhes extras nos ajudam a reproduzir o problema.',
137
137
  'server.error.report.modal.send': 'Enviar relatório',
138
138
  'server.error.report.validation': 'Descreva o que aconteceu antes de enviar.',
@@ -220,7 +220,7 @@ export const CommonTexts = {
220
220
  'internal.server.error': "Si è verificato un errore inaspettato. Contatta l'amministratore.",
221
221
  'server.error.report.prompt': 'Qualcosa è andato storto da parte nostra. Clicca qui per raccontarci cosa è successo.',
222
222
  'server.error.report.modal.title': 'Segnala un problema',
223
- 'server.error.report.modal.subtitle': "Descrivi cosa stavi facendo quando si è verificato l'errore. Useremo il tuo messaggio per indagare.",
223
+ 'server.error.report.modal.subtitle': "Descrivi cosa stavi facendo quando si è verificato l'errore. Includeremo anche l'output recente della console e uno screenshot di questa pagina.",
224
224
  'server.error.report.modal.placeholder': 'Cosa stavi cercando di fare? Dettagli aggiuntivi ci aiutano a riprodurre il problema.',
225
225
  'server.error.report.modal.send': 'Invia segnalazione',
226
226
  'server.error.report.validation': 'Descrivi cosa è successo prima di inviare.',
@@ -1,33 +1,19 @@
1
1
  import type { ServerErrorContext } from '../../common/AppToast/ToastMessage';
2
+ import type { ConsoleLogEntry } from '../../features/ServerErrorReport/consoleCapture';
3
+ import '../../features/ServerErrorReport/consoleCapture';
2
4
  export type ReportSendContext = {
3
5
  locale?: string;
4
6
  corpusUid?: string;
5
7
  corpusName?: string;
6
8
  };
7
- type ClientContext = {
8
- timestamp?: string;
9
- timezone?: string;
10
- appUrl?: string;
11
- environment?: string;
12
- locale?: string;
13
- corpusUid?: string;
14
- corpusName?: string;
15
- userAgent?: string;
16
- viewport?: string;
17
- screen?: string;
18
- referrer?: string;
19
- pageUrl?: string;
20
- };
21
9
  type ErrorReportPayload = {
22
10
  userMessage: string;
23
11
  errorContext?: ServerErrorContext;
24
12
  sendContext?: ReportSendContext;
13
+ consoleLogs?: ConsoleLogEntry[];
14
+ screenshot?: string | null;
25
15
  };
26
- declare function send({ userMessage, errorContext, sendContext, }: ErrorReportPayload): Promise<import("axios").AxiosResponse<any, {
27
- userMessage: string;
28
- errorContext: ServerErrorContext | undefined;
29
- clientContext: ClientContext;
30
- }, {}, any>>;
16
+ declare function send({ userMessage, errorContext, sendContext, consoleLogs, screenshot, }: ErrorReportPayload): Promise<import("axios").AxiosResponse<any, Record<string, unknown>, {}, any>>;
31
17
  declare const ErrorReportService: {
32
18
  send: typeof send;
33
19
  };
@@ -1,3 +1,4 @@
1
+ import '../../features/ServerErrorReport/consoleCapture';
1
2
  import { createApiClient } from '../api/createApiClient';
2
3
  function getFunctionsApiBase() {
3
4
  const explicit = import.meta.env.VITE_APP_FUNCTIONS_API;
@@ -44,20 +45,27 @@ function buildClientContext(sendContext) {
44
45
  }
45
46
  return clientContext;
46
47
  }
47
- function send({ userMessage, errorContext, sendContext, }) {
48
+ function send({ userMessage, errorContext, sendContext, consoleLogs, screenshot, }) {
48
49
  const functionsApiBase = getFunctionsApiBase();
49
50
  if (!functionsApiBase) {
50
51
  return Promise.reject(new Error('functions.api.unavailable'));
51
52
  }
53
+ const body = {
54
+ userMessage,
55
+ errorContext,
56
+ clientContext: buildClientContext(sendContext),
57
+ };
58
+ if (consoleLogs?.length) {
59
+ body.consoleLogs = consoleLogs;
60
+ }
61
+ if (screenshot) {
62
+ body.screenshot = screenshot;
63
+ }
52
64
  return createApiClient({
53
65
  baseURL: functionsApiBase,
54
66
  attachJwt: true,
55
67
  serverErrorHandling: 'none',
56
- }).post('/error-report', {
57
- userMessage,
58
- errorContext,
59
- clientContext: buildClientContext(sendContext),
60
- });
68
+ }).post('/error-report', body);
61
69
  }
62
70
  const ErrorReportService = { send };
63
71
  export default ErrorReportService;
@@ -5,32 +5,37 @@ import { useTranslation } from 'react-i18next';
5
5
  import AppModal from '../../common/AppModal/AppModal';
6
6
  import AppTextArea from '../../common/AppTextArea/AppTextArea';
7
7
  import ErrorReportService from '../../configs/services/ErrorReportService';
8
+ import { captureTabScreenshot } from './captureTabScreenshot';
9
+ import { getConsoleLogs } from './consoleCapture';
8
10
  import './styles.scss';
9
11
  export default function ServerErrorReport({ errorContext, corpus, onClose, onSent, }) {
10
12
  const { t, i18n } = useTranslation('common');
11
13
  const [userMessage, setUserMessage] = useState('');
12
14
  const [sending, setSending] = useState(false);
13
15
  const [sendError, setSendError] = useState(null);
14
- const handleSend = () => {
16
+ const handleSend = async () => {
15
17
  if (!userMessage.trim()) {
16
18
  setSendError(t('server.error.report.validation'));
17
19
  return;
18
20
  }
19
21
  setSending(true);
20
22
  setSendError(null);
21
- ErrorReportService.send({
22
- userMessage,
23
- errorContext,
24
- sendContext: {
25
- locale: i18n.resolvedLanguage || i18n.language,
26
- corpusUid: corpus?.uid,
27
- corpusName: corpus?.name,
28
- },
29
- })
30
- .then(() => {
23
+ try {
24
+ const screenshot = await captureTabScreenshot();
25
+ await ErrorReportService.send({
26
+ userMessage,
27
+ errorContext,
28
+ sendContext: {
29
+ locale: i18n.resolvedLanguage || i18n.language,
30
+ corpusUid: corpus?.uid,
31
+ corpusName: corpus?.name,
32
+ },
33
+ consoleLogs: getConsoleLogs(),
34
+ screenshot,
35
+ });
31
36
  onSent();
32
- })
33
- .catch((error) => {
37
+ }
38
+ catch (error) {
34
39
  console.error('Failed to send server error report:', axios.isAxiosError(error)
35
40
  ? {
36
41
  message: error.message,
@@ -40,10 +45,10 @@ export default function ServerErrorReport({ errorContext, corpus, onClose, onSen
40
45
  }
41
46
  : error);
42
47
  setSendError(t('server.error.report.send.failed'));
43
- })
44
- .finally(() => {
48
+ }
49
+ finally {
45
50
  setSending(false);
46
- });
51
+ }
47
52
  };
48
53
  return (_jsx(AppModal, { title: t('server.error.report.modal.title'), subtitle: t('server.error.report.modal.subtitle'), close: onClose, confirm: handleSend, disableConfirm: sending || !userMessage.trim(), confirmLabel: t('server.error.report.modal.send'), className: "server-error-report-modal", children: _jsxs("div", { className: "server-error-report-modal__body", children: [_jsx(AppTextArea, { value: userMessage, onChange: (e) => setUserMessage(e.target.value), placeholder: t('server.error.report.modal.placeholder'), rows: 6, disabled: sending, "aria-label": t('server.error.report.modal.placeholder') }), sendError && (_jsx("p", { className: "server-error-report-modal__error", role: "alert", children: sendError }))] }) }));
49
54
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Best-effort tab screenshot for error reports.
3
+ * Hides the report modal during capture so it is not in the image.
4
+ */
5
+ export declare function captureTabScreenshot(): Promise<string | null>;
@@ -0,0 +1,56 @@
1
+ import html2canvas from 'html2canvas';
2
+ const MAX_WIDTH = 1600;
3
+ const JPEG_QUALITY = 0.85;
4
+ function hideReportUi() {
5
+ const hidden = [];
6
+ const modal = document.querySelector('.server-error-report-modal');
7
+ const root = modal?.closest('.MuiModal-root') ||
8
+ modal;
9
+ if (root) {
10
+ hidden.push({ el: root, display: root.style.display });
11
+ root.style.display = 'none';
12
+ }
13
+ return hidden;
14
+ }
15
+ function restoreHidden(hidden) {
16
+ hidden.forEach(({ el, display }) => {
17
+ el.style.display = display;
18
+ });
19
+ }
20
+ function toJpegDataUrl(canvas) {
21
+ let out = canvas;
22
+ if (canvas.width > MAX_WIDTH) {
23
+ const scale = MAX_WIDTH / canvas.width;
24
+ out = document.createElement('canvas');
25
+ out.width = Math.round(canvas.width * scale);
26
+ out.height = Math.round(canvas.height * scale);
27
+ const ctx = out.getContext('2d');
28
+ if (ctx) {
29
+ ctx.drawImage(canvas, 0, 0, out.width, out.height);
30
+ }
31
+ }
32
+ return out.toDataURL('image/jpeg', JPEG_QUALITY);
33
+ }
34
+ /**
35
+ * Best-effort tab screenshot for error reports.
36
+ * Hides the report modal during capture so it is not in the image.
37
+ */
38
+ export async function captureTabScreenshot() {
39
+ if (typeof document === 'undefined' || !document.body) {
40
+ return null;
41
+ }
42
+ const hidden = hideReportUi();
43
+ try {
44
+ const canvas = await html2canvas(document.body, {
45
+ logging: false,
46
+ useCORS: true,
47
+ });
48
+ return toJpegDataUrl(canvas);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ finally {
54
+ restoreHidden(hidden);
55
+ }
56
+ }
@@ -0,0 +1,11 @@
1
+ export type ConsoleLogEntry = {
2
+ t: string;
3
+ level: string;
4
+ msg: string;
5
+ };
6
+ declare global {
7
+ interface Window {
8
+ __tychoConsoleCaptureInstalled?: boolean;
9
+ }
10
+ }
11
+ export declare function getConsoleLogs(): ConsoleLogEntry[];
@@ -0,0 +1,56 @@
1
+ const MAX_LOGS = 60;
2
+ const MAX_MSG = 400;
3
+ const logs = [];
4
+ function serializeArgs(args) {
5
+ let msg;
6
+ try {
7
+ msg = Array.prototype.map
8
+ .call(args, (a) => {
9
+ if (typeof a === 'string')
10
+ return a;
11
+ if (a instanceof Error)
12
+ return a.stack || String(a);
13
+ try {
14
+ return JSON.stringify(a);
15
+ }
16
+ catch {
17
+ return String(a);
18
+ }
19
+ })
20
+ .join(' ');
21
+ }
22
+ catch {
23
+ msg = '[unserializable]';
24
+ }
25
+ if (msg.length > MAX_MSG) {
26
+ return `${msg.slice(0, MAX_MSG)}…`;
27
+ }
28
+ return msg;
29
+ }
30
+ function push(level, args) {
31
+ logs.push({
32
+ t: new Date().toISOString(),
33
+ level,
34
+ msg: serializeArgs(args),
35
+ });
36
+ if (logs.length > MAX_LOGS) {
37
+ logs.shift();
38
+ }
39
+ }
40
+ function installConsoleCapture() {
41
+ if (typeof window === 'undefined' || window.__tychoConsoleCaptureInstalled) {
42
+ return;
43
+ }
44
+ window.__tychoConsoleCaptureInstalled = true;
45
+ ['log', 'warn', 'error'].forEach((level) => {
46
+ const original = console[level];
47
+ console[level] = function (...args) {
48
+ push(level, args);
49
+ return original.apply(console, args);
50
+ };
51
+ });
52
+ }
53
+ installConsoleCapture();
54
+ export function getConsoleLogs() {
55
+ return logs.slice();
56
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tycho-components",
3
3
  "private": false,
4
- "version": "0.42.3",
4
+ "version": "0.42.4",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {