From a15efcc6d02e25a0fc6bfddee4a6a9fecedae6a6 Mon Sep 17 00:00:00 2001 From: Will Hunt <2072976+Half-Shot@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:29:18 +0000 Subject: [PATCH] Allow local log downloads when a rageshake URL is not configured. (#31716) * Add support for storing debug logs locally and allowing local downloads. * static * Comprehensive testing for bug report flow. * Driveby cleanup of typography * fix i18n * Improvements to UX * More testing * update snaps * linting * lint * Fix feedback * Fix boldnewss * fix bold * fix heading * Increase test coverage * remove focus * Don't show the FAQ depending on whether you can submit feedback. * move reset * fix err * Remove unused * update snap * Remove text * Bumping up that coverage * tidy * lint * update snap * Use a const * fix imports * Remove import in e2e test * whoops --- docs/config.md | 1 + playwright/e2e/feedback/rageshakes.spec.ts | 139 ++++++++++++++++++ .../rageshake-locally-linux.png | Bin 0 -> 23842 bytes .../rageshake-via-url-linux.png | Bin 0 -> 41250 bytes src/@types/global.d.ts | 2 +- src/IConfigOptions.ts | 11 +- .../views/dialogs/BugReportDialog.tsx | 125 +++++++++------- .../views/dialogs/FeedbackDialog.tsx | 1 + .../dialogs/GenericFeatureFeedbackDialog.tsx | 2 +- .../views/elements/BugReportDialogButton.tsx | 43 ++++++ .../views/elements/ErrorBoundary.tsx | 14 +- .../views/messages/TileErrorBoundary.tsx | 24 +-- .../tabs/user/HelpUserSettingsTab.tsx | 11 +- src/models/Call.ts | 3 +- src/rageshake/submit-rageshake.ts | 41 +++--- src/settings/Settings.tsx | 17 +-- src/utils/Feedback.ts | 4 +- src/vector/rageshakesetup.ts | 41 ++++-- .../views/dialogs/BugReportDialog-test.tsx | 11 +- .../BugReportDialog-test.tsx.snap | 77 ++++++++++ .../elements/BugReportDialogButton-test.tsx | 53 +++++++ .../BugReportDialogButton-test.tsx.snap | 29 ++++ .../LabsUserSettingsTab-test.tsx.snap | 15 +- test/unit-tests/models/Call-test.ts | 21 ++- test/unit-tests/submit-rageshake-test.ts | 59 +++++++- test/unit-tests/utils/Feedback-test.ts | 45 ++++-- test/unit-tests/vector/rageshakesetup-test.ts | 65 ++++++++ 27 files changed, 692 insertions(+), 162 deletions(-) create mode 100644 playwright/e2e/feedback/rageshakes.spec.ts create mode 100644 playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-locally-linux.png create mode 100644 playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-via-url-linux.png create mode 100644 src/components/views/elements/BugReportDialogButton.tsx create mode 100644 test/unit-tests/components/views/dialogs/__snapshots__/BugReportDialog-test.tsx.snap create mode 100644 test/unit-tests/components/views/elements/BugReportDialogButton-test.tsx create mode 100644 test/unit-tests/components/views/elements/__snapshots__/BugReportDialogButton-test.tsx.snap create mode 100644 test/unit-tests/vector/rageshakesetup-test.ts diff --git a/docs/config.md b/docs/config.md index 0cb3c702a3..c8773544fe 100644 --- a/docs/config.md +++ b/docs/config.md @@ -407,6 +407,7 @@ If you run your own rageshake server to collect bug reports, the following optio 1. `bug_report_endpoint_url`: URL for where to submit rageshake logs to. Rageshakes include feedback submissions and bug reports. When not present in the config, the app will disable all rageshake functionality. Set to `https://rageshakes.element.io/api/submit` to submit rageshakes to us, or use your own rageshake server. + You may also set the value to `"local"` if you wish to only store logs locally, in order to download them for debugging. 2. `uisi_autorageshake_app`: If a user has enabled the "automatically send debug logs on decryption errors" flag, this option will be sent alongside the rageshake so the rageshake server can filter them by app name. By default, this will be `element-auto-uisi` (in contrast to other rageshakes submitted by the app, which use `element-web`). diff --git a/playwright/e2e/feedback/rageshakes.spec.ts b/playwright/e2e/feedback/rageshakes.spec.ts new file mode 100644 index 0000000000..58476f04ac --- /dev/null +++ b/playwright/e2e/feedback/rageshakes.spec.ts @@ -0,0 +1,139 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { test, expect } from "../../element-web-test"; + +function formDataParser(data: string, contentType: string | null): Record { + const [, boundary] = + contentType + ?.split(";") + .map((v) => v.trim()) + .find((v) => v.startsWith("boundary=")) + ?.split("=") ?? []; + if (!boundary) { + throw Error("No boundary found in form data request"); + } + const dataMap: Record = {}; + for (const dataPart of data.split(boundary).map((p) => p.trim())) { + const lines = dataPart.split("\r\n"); + const fieldName = lines[0].match(/name="([^"]+)"/)?.[1]; + if (!fieldName) { + continue; + } + const data = lines.slice(1, -1).join("\n").trim(); + dataMap[fieldName] = data; + } + return dataMap; +} + +test.describe("Rageshakes", () => { + test.describe("visible when enabled", () => { + test.use({ + config: { + // Enable this just so the options show up. + bug_report_endpoint_url: "https://example.org/bug-report-place", + }, + }); + test("should be able to open bug report dialog via slash command", async ({ page, app, user }) => { + await app.client.createRoom({ name: "Test room" }); + await app.viewRoomByName("Test room"); + const composer = app.getComposer().locator("[contenteditable]"); + await composer.fill("/rageshake"); + await composer.press("Enter"); + await expect(page.getByRole("dialog", { name: "Submit debug logs" })).toBeVisible(); + }); + + test("should be able to open bug report dialog via feedback dialog", async ({ page, app, user }) => { + const menu = await app.openUserMenu(); + await menu.getByRole("menuitem", { name: "Feedback" }).click(); + const feedbackDialog = page.getByRole("dialog", { name: "Feedback" }); + await feedbackDialog.getByRole("button", { name: "debug logs" }).click(); + await expect(page.getByRole("dialog", { name: "Submit debug logs" })).toBeVisible(); + }); + test("should be able to open bug report dialog via Settings", async ({ page, app, user }) => { + const settings = await app.settings.openUserSettings("Help & About"); + await settings.getByRole("button", { name: "Submit debug logs" }).click(); + // Playwright can't see the dialog when both the settings and bug report dialogs are open, so key off heading. + await expect(page.locator(".mx_BugReportDialog")).toBeVisible(); + }); + }); + + test.describe("hidden when disabled", () => { + test("should NOT be able to open bug report dialog via slash command", async ({ page, app, user }) => { + await app.client.createRoom({ name: "Test room" }); + await app.viewRoomByName("Test room"); + const composer = app.getComposer().locator("[contenteditable]"); + await composer.fill("/rageshake"); + await composer.press("Enter"); + await expect(page.getByRole("dialog", { name: "Unknown command" })).toBeVisible(); + }); + + test("should NOT be able to open bug report dialog via feedback dialog", async ({ page, app, user }) => { + const menu = await app.openUserMenu(); + await expect(menu.getByRole("menuitem", { name: "Feedback" })).not.toBeVisible(); + }); + test("should NOT be able to open bug report dialog via Settings", async ({ page, app, user }) => { + const settings = await app.settings.openUserSettings("Help & About"); + await expect(settings.getByRole("menuitem", { name: "Submit debug logs" })).not.toBeVisible(); + }); + }); + + test.describe("via bug report endpoint", () => { + test.use({ + config: { + bug_report_endpoint_url: "http://example.org/bug-report-server", + }, + }); + + test("should be able to rageshake to a URL", { tag: "@screenshot" }, async ({ page, app, user }) => { + await page.route("http://example.org/bug-report-server", async (route, request) => { + if (request.method() !== "POST") { + throw Error("Expected POST"); + } + const fields = formDataParser(request.postData(), await request.headerValue("Content-Type")); + expect(fields.text).toEqual( + "These are some notes\n\nIssue: https://github.com/element-hq/element-web/12345", + ); + expect(fields.app).toEqual("element-web"); + expect(fields.user_id).toEqual(user.userId); + expect(fields.device_id).toEqual(user.deviceId); + // We don't check the logs contents, but we'd like for there to be a log. + expect(fields["compressed-log"]).toBeDefined(); + return route.fulfill({ json: {}, status: 200 }); + }); + + const settings = await app.settings.openUserSettings("Help & About"); + await settings.getByRole("button", { name: "Submit debug logs" }).click(); + const dialog = page.locator(".mx_BugReportDialog"); + await dialog + .getByRole("textbox", { name: "GitHub issue" }) + .fill("https://github.com/element-hq/element-web/12345"); + await dialog.getByRole("textbox", { name: "Notes" }).fill("These are some notes"); + await expect(dialog).toMatchScreenshot("rageshake_via_url.png"); + await dialog.getByRole("button", { name: "Send logs" }).click(); + await expect(page.getByRole("heading", { name: "Logs sent" })).toBeVisible(); + }); + }); + test.describe("via local download", () => { + test.use({ + config: { + bug_report_endpoint_url: "local", + }, + }); + + test("should be able to rageshake to local download", { tag: "@screenshot" }, async ({ page, app, user }) => { + const settings = await app.settings.openUserSettings("Help & About"); + await settings.getByRole("button", { name: "Download logs" }).click(); + const dialog = page.locator(".mx_BugReportDialog"); + await expect(dialog).toMatchScreenshot("rageshake_locally.png"); + const downloadPromise = page.waitForEvent("download"); + await dialog.getByRole("button", { name: "Download logs" }).click(); + const download = await downloadPromise; + await download.cancel(); + }); + }); +}); diff --git a/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-locally-linux.png b/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-locally-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..c6a06a5035d86f4645285fb851bceb1f907edcac GIT binary patch literal 23842 zcmdSBWl)`6*QFaAf`{M^K|*kMf(Hoh?(XjH4#6FQySuwvaChIhd*6AU_v=1i*Ev5< z^{=KVY6F|CYt1$1HO3%RMoJX%BhJSUA3h+8{ScD-@BzFFxV#Gs4t&KvhpPVY0p)|3 z5Wj*;Iw%89LvkEMvsnAe|=v_w9kA@lw3E9w*L{DwO z_%PVL_g2XX_qCQr_n~j^_m5r`V<}9=MiXO9Oq1M`9QPQncc-|bD3JUpkg=HOGrWcd z1~oRiQADtQM6d!#VV~bbEs3Cj%fM%|j0|N@u$b?^zoQMq_}YU@{Nq30YK37c>wwAq z^Ek}Y=hCXoVn7}A;Yi!9uhks8H%qa!lBwZr{Bli$Dm=>{L={fg|L z$K2;lph6^`K&_>~T_SY3eT-v?`fwNNezgnNnbqOK&?2E+n;Q&Ks#HFL`u7=LZa_yx z5PpVD5T|N=ak>aLH8sV|)i0P#c6@*HXR}^k=8{_8YiyUv5x@hk&5!bK88*VMT%u5n z^yCKi<`p!69~URz(@QFq1;@h3youK4a@pg3pLuaHB`CXa$hFH4c{*Fh3%0fx{VgaQer`9GgC7gSKFJ#%iiE<%}!g0`FOLr z5?0GUP5PoUT0G+)G2gnKuTNJRa|ivTGr1ji_GwHc>dodYR-2}~d=pzZProomM7Gy? z2GiY?L=*BI*O{}~vh+}&)8BDoSj=qO?GN`d}f`gogGQf8nrM zJHBGvf0d8Vl}a?QIOOvc`lP>PVq<;T9qB1#N;Mn+1dGOR31BawYM z=}N;<PLJI~sm{}7wbVZ)uaDQp?xIQA*@7Np{xHaw93H!iZ?C>53+3CG zv%%2>k+{a*d&exiWJ-n};QmNaxLj^ErSOEE#^+uzqL^=^oyd^T#N%ZtGtF6gGyCOC z#^ZbT{_PaJyu8#Pyw8D_>a6pX+R@U@*4{)T<3mF_!IaF0n>k;t;_UX18fUzJoqWE; zGLf44&1$jQ<~sAlTCQ&U&rTy8y2U9hE@puL&Zh%LEkRl?%5cgroc}ve*cxPOK40eB zdw;$Wjm`D;bc+icZjS3=g&O-FkV~tv+k22pTjdkW4|DlPyDdb!*$mNwSYKqt>DXk1 zMEBw%nIkxE;sq+)Eju#k3+BRR9huM-6)DkdiT*`n&7?o+0PJl<#2 z=jCa5APSxHsAR>`1!18~H=j&wl;2Z=V z;J3!jEM~JdyRfBaYpqmNRNE-W)37J9CL$fHVsWdd+Q=Nn(nstlUN411nPd;gI zZ{!#Q<8HL7AuGBfpOfwJGRXgZA*6RLeLviGwtms-@Caq81a=<%kG#A*5@;#kE(rHc z(08ox)ofy=g9+g-F$V8$y)1n7l?FwBx7e5q=x`DTqypUw2}Yl1e?!3Bp2bC( zwj0rCLVaOpyYwNX>a2$9dO;%Ev!|UwX|sijgxVfpU#vBmeuLM?50v)_tnMUacfm@k z4<%%Es46Jn9Kq+XW5+Nx`7~1~x%KU0s}}~ejLPeJ#rzo_vQ4S%Ek1L?uvwwOa!uj4 zi){cZli7_*-d2E*dM1-I82tU|it*APVQRehE5ik=eG}Ry*#@f>2Cr2tp$gST-vs!| z5ns>NlwV*lVXo5f>>7Qw?H-RGnNG8pUOn2DW~lt35q*?=k<4bRz{(i63-JGz-3Qa% zn(tM@;iGOI#2g&ht?v*pekx<}Un_pPe^}g@%;fM7tRD4#zO&rw33MeDauNEHwC58N z6~(XfE1dL=oQ#Yg`8yFgIYtWUl$5%Js;X+6g8`RYJd5{YzCFboHvAz*vl3PX2FBEs z%t1X&cI8HgvNRJ0ugi~k=!8a)@fNQmVS2V>HLX+OO0pQn69cc9|#V%mL0DK0Li_K~z~w%c28bM3wB`}pyr z+o@x}JgdcGw$$k|b%bFA8qZXWrBqYQ-djjWgaQ0Y7uf5PJnUF&_cIFuBG-@jC`Z|C zu&z`Vv#Jm8)Pazb9V>UYw+>`dm*_zWe$fG^yMOk+6-s5PHhA<0ghsu>Ao;fYW;~_t zarrRzZPX;rBO_b0n#gF2iG`x|#GRiHmK4{=HeqDqV7Odt5-05?_(b)OSXP#jW`0V= zsBB?SP+aLM=rQA3w(>ZrFuFsUxNryv$_Sho{CnyMy%8tD?|9AYL6<0&e#GC_wEM`V zAV;NKS7d;Mu1Lk$`@x+t+uzGQ8Kp`ryYGe0=Dz ztTz3ySwb)r!M=k%^`JCw`0El2wRVTc#BufaKdsKxn?S+o?jhTj_OF5O*Z8So7;o>v{9AVA#!Zu}3Ar4SJL8r?{Gv4>aPxi>F}|zA3sr9HJ6J==aW#;~tD_ zH@$zCQBzyKJz-mnqg)k7iptEFz!-7akN7v~&zm`&wab@7*?KL?4ahJMLqqznibFAHo%xLhA<_>MJ*RXdsYA`x0qK=^7 zSnBUl-uYwwW8cXFoO+{ctxaW_GDG!^Qb}HqVf)AKOp8t5#I7;qDo*ByHxiQbgpjZn zo7>bMvh-talYTtZkD#UNkdUEW>e8UlYT_h9X0zECsEJJYBz8wyZa=7015>JCBz)aq zQc;!Dwc1i=I{OT#E&pYbu!ZGQBFr1uf?yoPS!nj8}_!Mp=2~bi0X-jrt zTq#jh@SoL?4CpitkB({r2?wqxGB|4>?GI>P9xh4Yd!-rrd7g>IDPgL3s;WY(QQ<)s zm%RcZdy>k^2JR;Fbz~41nsR?MUk*Soj`+fY{)A&hEsWJZk4uXI|L92ct-(+)zz0 zvA*k&x$AUvg+9y2!QncH#Fv=ueqRAnPrzXwn#bKA*G^7NU2btKjW#7@vs|KjtN5y) zzII3|8pYq(Xa3QOrVZ$X8!A{&HWrt(Rg9G-y=Kd+}ms+D)N^ERs zV3sTP@mwjFiH>*TpLwI*W_yy+?jxogw9|~I`}6ykrR!eN=u2zki+Eba)OML;PFFJv z5SgbZPcmf`KD0|iktU;oGei+xnN*1pI@xAT?2Zj zU%!4?t+zxXdw(oF`dFDyquzir**#PR;jW2>g>`s+NpJeyS4UT$RPat%`q6J?SFBRI z6ApnBwl$K3$XC zURC?7&cL*xxJy5%HTqnN{WCkU!?6kHj6TjYZ7~{!)RlCK4`;o(^>S5WdM823O z4Cn@sVu^z3dYP&8HNok!slmU0b zzSqh=GrG4Wduy%Ev9Ib_KlXQkUeX=rjD(yO!wm9lbi(R$1m&lXkcZYwjaq9hTpk|! zbedi-4{)ilL5V?zrsI;_Q%mvNqZgt&r=?xzD;FZBmU0>*!@4R^Iu~hecs<=8wHXW>p!C(3c}Zae!veK z^!fk#n@R3@U^BZ}#Hl~7jc$l~ORh$n5Y>!1*e*U}3PNn$_d-6K;Vu(dw0U>dXm=|w z7irz;rfA|5l9|d8kyInO_{y`^RkYJ1EYYM*PFPE|IhN5_)fh;YwDItC`p2yBe!Cz3 ztdRb6(Q$nF){pk_Vhdv6Ic2I!n=a#lt^2`ps)*wrK7xR0V%Z1*{{3e6{h3c?ukgQE zz^3sVAFty6$U>zyMQd}T?9A-cEHKNbMZ}XA_=)ami|XEGB^C@=d27FG^5%*zeg3M^ z_xs!j{hP9V07@pWXAr*&F!?MEmnfAl*NMfc)tlLRr+(@WZ#120;rEALl}1IWYBarO zef%}|j=fDoO5td>U^d<68u$^tnXfyGCSwSV$8!e}m(9L27@X@7Grd?Y9T?~CPm$9> z$J_0e@;L&5ebTReUGFQyPP&v|zaG`ihlW ze;6~9qUX~q<6to{G5dYm!_~PmR|!R|Bw!-Nly0-#Nkj@ND6y;u;ip@#b&Qjcwc*Mv zxzw&C(P=A{Yc2GaIfpA*imFBPIvm2Xp3xWC{c*iNTLZ?YR;Tl&n);T=^0i_4!bkbV z%azD9SpQ!+wuSh-UjD%d!LcbBey63;!tX{j%ZJCXReJO_uD(C^eU?x5*2 zG+R6Z`+KG8T}N3*!31qw-+cueEN&;p#)MvgKBVXrI0~XBDkwj*oi)y6t?6*BA;CJF ztfQ?!B@X*`5oxUCshY1okEz=8dc6!Cpy0ZP)_kqLzbEwaW4fBAynaOFXVu@%^J1tUqAQ{1VR>i&uX4OUTQ~*(*W62Cj%^Z97ec zYIl0YuF-L*i>Td)l3FE+Na(dTn^YHe_`;3l%@j-;)C|6|qYOkkcbyImi7GgFaMM}n zy!&SR`Umq95Lq)YBn2eW>X%(+D=qef4R*Er{bWDNRq+s_-$c@{>HFd_GJ906$YMN^ z`As%4AQX#cGCZ=b+8f{=H%Mpj`)(nJz|{X(mgwe&h+_eDPEe7+I)>dGWu6FkcUBQs zxD6fq0mCJP7Me)zx1cnGoldps;-F`cB0L^KQbXtDvLV-IK6$%!!0gr zPJ)bjDF2t}AEP3ASK8Hfl1WzkltuDM1j(Q5+l7{0lGZ$3qu_13ed6K~cR6oya-Z88 zpD%{<=4905f20Za4J#_CYIn)H?+i#nep$WefZ=tyBq8X9bd13guSrrcEvG2YxCVhE zprs3NK|r$%RV3= z*qfbBd#@o^Y9u+vzfifMQ%I{3!t44}=l}Y!#AN*COB*($fe^6yDB=KIQl{EEvDy~A za79L=&YIOpf1dG;>Vxs=6tRCIow`P?5h8UkCY=cB8-SA_pphQa-&`j^DLVsE-8Sv- zM6Fv>g8G9}0VOWGHk;{71hSa46>9BDBuh<@lxVsr9&t zZnSKyj4JITqf^5I$PyNo2lnZ_`?xF?UzqFcHJY+ z0DEe#HSYNn;)QL>6lA??zlB;TD5&h>`gFEp`N<)yU#DWHU+uJe8$+HV~L?lb9CO=dvj9h>6 zdDwX09?!{$i-TDN{M)aJlH(=_Yk+g*v-RD8<4L+0BdATS|xHS*gED*qqVF&STrbWTE==*&ko6+w$c_KrE0QFv~g1(258(3m`2W~ zwn(+KB)0H>JU>j&%!KFj8*(SPrljIjszx^V>vZ?@Z;$3c((eey9M2asc`kp3oU#fL z%0MRIwqRSv5CjRT;XtF##O!`P;@+ZokBN~_hIUWAXUBB?dAr0r~Pk~v`5zU2lnrjjgh2>EFkCfo*|d^y-3Pb1!JcZ_==YnBQ< zRQ)p0vQ(WrJa4(ucz&^Woz##QM!N3CxUA}gDI_EU3?FBQTV1@KZXBGFe&ol8?3aC) zy^%Pq>01oX*9Vh7YFEt_RTepnDV~`M{pS=EMI*71(Zk{(imie3EJ0sYCnzkk9nbdc zXMY&F4QzApGLomqS$Ycf-cx-kgEOg)$s)gcA%v6g>yzq_QH;5@t=bvOt1pMm?98l; zeQSE&lVUJ5@K7|MpMTfk5}EaSTQ?pIi|ge2YgMN%9>zDNh|yo}j^h>*7IJ^MD9f?l zfg^aJP;5u!P%Sha;toWkSn^p3->ErO4c8z9hl-NYVr`$Nf86ncXj<#zN1>xX`3(D~ z1B*5Av0He#R|*mL{s)g32RD!vD~28t8291SYN7KbEnaf8!ycehh* z>JBvDC#tS4w@>xAAzK&ABoW11y}|de3gvQ8cG~%NhKc)Yt-F_KAXyY6MeLpi*K1aG z&$U7$X+lXHwxHuvhP8}yMq2aNUg$%@B_>wTY;!R8D~a*FSgv`Jlt>UXY;uYtABG3G zqt6$8BAuA)1mC4*p!1LUDaD%&^L^Yg4b+1~%v7~Bq}>uxO^yUwS2?{qTRGR^0}F;h zf~+^2t1%qKU=(4XhqesnrQEj{RY?#ys|F2I`Wh_OI{u8#^!338 zmt}Z*IO)*<7?B6UeAs9_)#-ft1i)jFI4tp^d3(NDFWMAevX?}os;JcJ4zqnG^rJw!XO0n&Og1Mg4MUB0L&319iPv z0w&mowQ+`?RMzR~>HU*2G&Fay7rPrU1u4i)SRWRsKf{GWfx0;@?&n&rdsn~xs9u?f zrl+djch&3>dNki!tBE^29>s-rT`v(_ABe%|6YW?y^g7@XIB%}@Mn$8u0((GyqlQ%t#Z~f+n?FL0)>Ame>4sqeDO~XS_*R#rL13;_(%f!9JiwQ$zE7^T<@$Y z0lgcG#ngAjddbELO_er*>E9>w)-M;ti1Yom2R$=0e7#k~>Wg}M`qB8v1B%;^d~Po) z9u4hwdnHYvoUbvxz_A5cA29{JE404ho%g%!H%VUXs3pDsP8!W5-i!rCSe=sG`A^ zrg4+dAiwNJUk3KWHCPpu$Us)lUV?X#y0?uU^78New%O{j1?Gx1S42mDU;KF@G<{NR zmiXzoU*GM^Z*s&g(R5rm6Xbes+^nIaOG^6MH-qg_f0>xt~xaYg0 z52@Y1LbZE_Ar~-csZ~erS3C6fhHgLm2*8ki&ZIxoD_tnVHD^oR@VewUP~$8u%NE}^ z*cqUK$D|AFfWuV?MslMs(o5=sgDKeI^@P#U>sg=p?OX}=`rO?-8anrryV~WMG(wgl zGls>h;)i({w|Ej=X_g63D8geEpuWh<$5mC;9?B;}dV0Kqzr1ehRW)EoKI}`TMd6tS zmb6G;Z(e@w+!;7Ls>h@&wcR09!elOBUW%(V^!g2ESm!TfxZ50n?ERX0sV{Hi z7sE5(cVs^8e)-Fg{xOUiEQ`0=GBQGzx^?-A)Jw5KpO1+Yg7piix3989ww;qAKpSr% z1)GHKAyrWeJ@A{zJ5m18E~=^PCWW_!)ZO_lfVjLKhNe$fJaqyCU!lY6T(+}t&jkhp zBg^qW!#?y>eLY@n-bv@UR&A@p88HddI6yIq-#-80rJ5jFfw^CuC(bXz_5KteKHk+_ zTgYH^j}M#9=8f8B!4GpcUle;kHS~%Nt0IwI(knY+m@=e8ZYI;o>tqM=7xSIuq@|78 zVD^5v09(;k(L|wDt@*OjM4!n!eiu4lp$U%!zTcrIB$R$&0KRtoc#Zk8Bc+-oWM-rq zC!_RpjBLLj43IYmpyLA>ZhP4w?h|f@wg#gK0#L7pru8}OgHi{yk!mR-UKR6{hHMVwm`$d1nh*4F(@;&!wE7ciEC8J74h%(fa~DDa8*)$6Ho*e{piAj5y1UmLLraW& z(>{N?Dg^Y1Kx@vM6B(`BraGK{dEHBsNROv1CW}>XkA%XhXnw${0_J3$+|95`U+8?g zoT55p5TVzAChiiqwo^sGGkMzFkiIGKMs@KZb~i4L`vgh%@3+QRK*8hrb21|GyPUxxm=uCa*HB9mCCmnO z*Ok)EQwKW4f}&kV7|txvwA`1?0?%+5G$ zRtPw#cO)j1<_l#3FsR1g6pH8vthnPn5qtnTuydu=K89LZQ2~8D4NQkCjSIrk>D(!= zeU2%NhS113^v>&kfAS@|v1{MIqwv<0!tTLx=B_n`(oP_$sF9H7N(H2IR(Z2Y-8O(S-P18Qv)7BBPFOJG+QVJiHYkH$*YXiYfny;`7=@9nM7kXX~Cv&9BU z`Dar};#>zqL&KrfTH{IG;tKUfBtkwLpS=oAupJ6cqzSvW?pv)Eg|EAl!;#EezCp8- zb}gR8mENY(h3@3I&j0>%4cl-(YE3ZBDBGEO<}1lMV@<0xij86zGNg+f@o(Q} zh`yPwF!<-YzEG)L5!FsH7yI2s+qc=6ZvO#k8y|uX%2wMeLN0$UCE- z8c|BLt75gYvy(8Ilob1uAa)`&Vt8ZF`5?9aWUa6T8y6c78L8Rxz-q#o%iF3A9tRVv zb8j^1{&r=qNtU#UJDW`~U#IY&`C6xV!Vw9TPAaHuL}WRh(icRgh{L%2xKOwX2us@S zZcyLwOCrgIf7auT8deyn6aA*u5USl~a;ds;V%1-c=QeLP>DF+$FArMD4&sMFw%lmIp5df$1Ftsn>7-MTV7Cs6uEN$?Y8<`~ z#*SS#f0S#|nVYTG-3WqivFX@#7h0@1UF^W#hLW|nbCQ%2^L-5GnVs3FsVoiOxxr?V z{XJO5Az%12@lpo{!Y>5L#>U}jwiza#|0fz{U9hUP{tRu*YzGGT>fE-@QZkGEQ-x;h z^ux)#B^HNX)58)p;_<=qn?@q|)+RmJ?@*^!@AGaz0emp-vm+`0Y7k!mXu;|W6U&@W z$%ytw3CWZr9|;ZiPIe-HNLl5K?AKMoJny)JP4Bu}Ft#;0u!`e8Jd%wtlOAlF>8 z*6KK1tKFwA;&cxO1;H;_kI4Di|MDiK%1{DRjN3t}_}55@;wYVVGpF7zr3tdiR{+e( zS7=m&JZ1xP1VyQ@)sD{22H^MyR7OK%bNI#WKJFV{!pP)B>Q$Nb*Zd+A-<)l^O>1#H z(kM4mGpu zaDz44+RSu$m`rd&CO0PPp`-sU;N+3b8(+l_`ev(;FfI(Ktd`WRL4Q&OmhnhreET3 z15f#)Ui1-b!zwbjfT?0|IdUKN(I*nDbU8)7dlB6R@{;WWra<;U%nAYqv47&zd>)|i zRA|%}h~8K5vE`7W(OB{%w{G3~GP%6@_1_b6>zs?CLMB!A+!2btRXbhk-Gn8S#{F8? zH6KE?^S=u?iK~OzrrSn-!J)vDFki4boN@kO5lTUPOmPb)Tb24^-R%qeCTcqT|WDYO4{H`*=o7-19wQ7xV;b-$pN? zjqX3v!)4<&4E4Vv#LMabnPTx6xpDOTwu);r9$R=B7riQ5W!R`15D${*yoFm4e&^Yp2&M^e@e3H%acR-eBYqgpZs77k~X= z9Z&k;OHI<(RXDo6uLoj3Tu6`oB$Apv`lpxb0e#>;sn-DX(*%nG&ouWzdYlPRb;oK& zKThC1cnf-Xyu{>?;;@)<=>8_Xm)8k1|Em(b4*}xfka`6n?ACeAzdml*-j$VIV=4%Wa6t7bulqh`axK?n;Eb@fi(}H7g5BsGhI9Yf%B6 zFS_nc*_%?rIt+sf^P8^CjO}t4);X+3IF09;tH?7 z&)XMJj86_t^c{CY@IKROB)?;y0nWA#w^y|nO&@AYy+f>)$_U|CVR^}0FBMF>Z{e6t>mY1;2 zmy`RG-VD#_%+gEKRcU)?!5*G<%&R|pSdD`2;4mn)2DsXKW}~_K8(Q@b&e(VDeF~v5 zx9z^D*SVOAi&JfY7fuHBWNwH^#&x!OK0~Y=IepJozt3DWBLch??z58r(&j1itu53~ zEH7}?2X-z=L^LsEQk+$k0Pscf1Y-~hceTFXk}g+ihQ<%(bU6M4(OJAX3ih3#Oeu#A(2k@uQ!V27l;5T&6O#Xbsx6Vu7< zPbvd_yL!By*ZVYA9InUATtfak7ap@vP9f`qJa&4C2kH6tJ%$8qj85t8sV%IG)%b?@ zyTkGr(X<0f=n&!?T|yo=)lO-=`WVX?<-T7}P8R4mYS9(gT!TJsoFm*Y@+IWN^J2VC zSGBkW0Wy^5<1bCZrCe)3N=SA!0+fVjBg48$pmb?cNcX>lPJ_`=lo%MT!!R06JAXI} zA&Xko?`k@Qfivbe?h1uqP z_)*Oh+@OJ^f5eDDi7zihpLkJ~NYaY(C55h{$GHm`b6|fB*zVv@m2W4dIe|WMY$3y^ zrKRO0ARE#A`LKrVaAD&;>IGY=+NxW8V5d*&KUdm@fXGR$+G25=dYk^e!Rq>KI(LO} zvLEn#rtfkFiY|>AwbR3OPb%Z* z1{sC=9btNUIya#0hu4xC;r$fL;(g7Qo-0*WR+q5f@cqtfFRgPC>!rZujk1|8H%4fd7>rFw)7(@353k!$QMj&>GEr zi%)xl2r@7-*!(q}Ye{du+U_pxB%hp$)!*0m`wCXO!>(Q^bX!&yGn;esVzn9e#;-b- zSR#}pmCd?g)n05cid?V19tPU5&G5RqWt83%xjqdl1(Fn->#l68u>4;pRU3QgkDez8Q)52@JdYYF~NURFouKcLVvtfryMAdf9C(Q2uLGES8Y!rhVb%|o#%dtVZexO zVl=O5fL!(2GDRuME5kj50jz7RsuIF83A zzn7J`$T41BRSUo5@VicDK}Tl$4O5b@fiK z_Q;ck&tK&rFEv{o^xG7&Ij0A#{Jh zSt*I^O~a};SZN>and<2U2$Bjyyxd+cg?6h&2p9Yp3xL}z5D2`Anf)y+Y(=EG=9H@D z`trH5p&_xRhLPDMoVUHFXCf5~x4%y-EFyxDl@;(ymM&;vdUng{6*WmZq1HzfGeFxr zI8cj!o;15oEdQ~?%cA~Vtl2tW-*nQponW43|LDt&?<)!C#8WGJR?D!t>lrog12|cXIe!v z#CU3|h^A{LWZ-B6EBr!qnl0(qqWNf_J^|98q18Nu?ykP>G`w24i(eLsyVI!O|3&X-7;db^YQ{*QF{F6eNs>*3SuIztst>~kABjjvRJAK1i3+jmeflWzLt>yJ3_Dx z&`Uyi;<<&wlE?(9s)#Oo5UR*^wbQPh=pke53I>_SR8dh6s*|BWOTl7jxQt_}z-+g7 zB@i>F49F5Qwh1E%OIxkZ!#fm<`VaJAg0HIfId6dBKKpBSX$}r-Kn7g5NT6bq`~g9_ z$F3S1#{skH7qWK$HCXgg`Bqk5K)LWc9^PYllG(N?&4K;?qQcv}4(tyG1{S@JFRPdl zTA30UdaeUou%$Un#*;e+Bbjw(;SfB~;0Ems6Ff1KN**l#(&f`fRM6BQ08%~i@!0QF0x49irr`~9hNRhG{K;35Vb zAHxfg`jg+k=lG<6!=vvSy}xwoWHqIwG3x`sZBNzk;xleflt|zH1=jRO zAqOvqmuR6iZVu;R<1^E_gEZ7`=PQH(T7&FKKX$fv=^axChi3|LSxvG_mJ8GuXwx0e z_*?+dmGaUK%nDNfXk^a(*m4f=mK#cnaU?OoO|fB30?ng8I9Tg;3^7DFx;#53L6o?_ zU|l_SccH~i`}B7*baaPEA*Dq)ex$ycaBY(Eac_KAPVfO~3prg)%xF+{q+wR%4ztdQ?a!h1lv35@fBGZf~0}BhiCxF~Ruc~mF;RqEKay#m>4RA;3yF%RAu=rGN(&2L@n30JC&sPu zqSrEYKcMF;3}Hn7$`|jJ>KcycF`dH@6*xxeisIuWS84$F71$$e2K;shtu=Ou>tiA@ zbt%DLQMN6~D4L~&{00iRW_E!BEY zeIJd9D{QbtXCZ9GylfdMV)H)W7``ou#BHS3X#4yT6J(0ddD#6M3MwX}24oC2xCmMz zu*nd>QDN(q_ff=fSGxRZrcg;6OGD0{EC%6^#P17>fKYQdgUggFgpvdxW-#N^SHojD#jdTDk}X&Yx)vXinU*r&e=z5j-jyK$-y= z6Msge^B`VS>BWjj`Kql=DD>0nJwq?VGy0o=Ws+?UQ29ckB^b+6w_^?MK}-fQv9TP0 z!Jns{keU;i?Vi%#R44G%>P)=OKu6)2xm$ly0lJO=(4RX!!%x!J>iwCz`sTrb*r{QO zi6RFR_y=PtliSs(8=VV$KuTeYRJ76hzMB`Wg>M#?_C1EU_ z!G019h;sPr1iN3|d@b{z+O6tHk8Xi=hJJD55Ve(p951O-P+y4{;GSwTorxtauQIjG z+9!Vg#9j`MNqb{Ywx22oECq1eSbtft6~l)ehJ}ff!`ilV#?`ynx|)jCg6Tb&!0oP#<5rfO)K4XCt~lQd43otC*S{cy=1Sd)eGqV zHmBppxE%EtARV z$vKz)K#zpaJ7PFo8Uep|1e@m@+QI+&4JGCgjxoO8`QD|Cq>bP5T`Xh^35nVw zXt{C0*OTSU41s!Ud%U0Y2dJ5v36Ab=^Ab(SGWrwiV>myxww*@ zR<&}pT+hVHw~cA^4TZjBqs0-Iinu_*2^Yw7B(Lq$sUTERXdnQN^8-4S-ThA(Am1o|lvG^RQE=_lf-}XL zu&SyUcO=jfaRMdjK!&9XKWm>C$|@G0%Y(sMz`UUWzjoXmCMmyICtIq@*>4#0wz1K< zLNymU36oMvTiZJA-YK{2fNbmCyt1^`u(WY%tP}bT`k%!wof`!d$R=tm%&z;h!sxRB9f+KGV`2i-t zH#d%cS125wB)~l-sSg_H?TEB9)ac-)j){}_Q1)-7s zme4E%M{}xrroWY6VS$^>0e#(3{X#!60&;Tgrrh+*%WjyU(*+{351Nv*G96TlEr*zM_0>{IO{AUhXFQM;kojN!aCl*iNp zk)-npiNiHDbgD9%_pESP6c4+GqI`L~OAxsjmv0(XE_f11@8nA)_)(6~D%=hAPj#ge z5)vYlsfdvCFKkuVV{QEgNDNY<4Y z$PMlhraWZM+>&ouSx^1eRJ6khO9GGeNc@WfRF|8}9G#OkkbczuH=>-XHclISKz{J` zW_2La9B6g{aOHHm&a^eIo*YFMarg9ey5%=FKP?JluWC=JbRsN~SZ|iQwaeXH=!$$7 zg7|Xz)+#hP9@UUWKfMk|BcE&J3xSl>AK=%iu&YM8wEn}M z=N%A-sVTJwET*d_(|)dgc34&{4m&|CQ#Uj)2>j%4t@mSDY%9R%4=UXS!A|Cf(-&EY zMp`bZ;hRH8Bx<(8x>&cmUK^>imh;)&xBzC={q`rlu%$NB43IIFECZGRKAtA0?F(CD-ZO{(FnVD5J#F&|f#N6<$Bqc#?&Lmy*h!r7VCA1Y+h^j~ru z?EOnkb2Y(%b$avzH&n*_>EOA^-`IX~@{6OY-wRoowZU@MtaqIf!9-`mm_^WcKjsp4(rLo!ZvJIOK zrzX2ME!S;$ZuE2UUF`Q%&~Onea$HNFQ0(j$MDg|QWul7}o3cTK1_i}4kkw_WTh@+l zr6mo{N_PF8FHUtK+PlxAw_8Ly7^NOvwJj}{7P5<%#%R~Yy9G$if9h0nh7v_c1^wf9 zKU3;*7*7kd^+Ip|icj8cw?q?t!I5#y$xi1$U(M$CjG|nOS$roHB#kBV!l#4-h?hU~ z9ichJBYNrY!4El$0!hBzAJyfe9t?qY`@=KjxGEIhr{{NflP{n8=)fUpG?>K1&UKi_ zU6t~_cf}%qYYDIQpGwjQ)oAv9_wDd<|M&Jc{<;0YM(d5cV|~>|tBiVcr7CNT*ld~1 z%HRJaulavmWIZs@{F5vA8%g*l8u0%wD`5Bdm>)+Wy-=j6eHR_|@@@;j*an=_D449x ze{O9E-qpL4VZ#+r!Dr*YDOcKdVspTk08`Ekxg?O1?*Hd;#A5FD42b|q+sUZ%e>2{8 zL^a<%-dZRFSa$lnDdSD%fJ_I3pz`g$WaF zrUIAMN~>;B9)OL4^3KaQDIPV>=j(v7++=>ANuyr6vbdbVP4>j|{0~$O^YZyiRo$MN zUw+-5t~jTsYt}Cx9o4bfWbNAm>5rvb*H=z3qo5gM0sIWjl_FCodgj}eqbzfNoAEM$ zva$h?T3=z#y%l*hWe>1|!)*Zx1kixwYO}dBAcvU50V3N*n`_#MW~EA3o*l}6v4G}f z$18*pouGk?nD_{7;D|tq?RBgDn5_AQ$Pvl`1_v6;KqV)HIJE}w%qc#r(yGCec)=)(+5rI{|v^|FF2Fe_7nu zSVeL162OEE#7GI4RxxRk@2~d0sjIiv)zxvI)LN|#=nBGQedL%gbgl$sVIVe@QJWnY zp6KF!UR8^`cW}Zp+&`_qpFVUg%VABy%nZ1$|GUrFuKmQv$J2)<&-Z2b^I87C%x{WUrn85X1hw=3 z#b8SIQ)C<veH^owdEs=`!^J4;LFphkv=F7juhFy94lx zj%W4wX|_0+mIXTLTZDi9N)ju(K$~Gh18!ur77dlvr9@!<2t+}FKm{@vnp~xpW;!D) zBM|UMR6s;Tbi{W2xLDh$P%KxfL`w&x@sZ|cmqAOdgtrPZGBHYoPB{s4G4thWB=ov% z_;Ok;4$Y=<1D{f)(~en<;w^zf2eP?VvR0_iXh;;Q0e0$=3bN>TfHhBLgPG6oSFole z;~X6y6K#TaKgb6k{0ekDneF}$7SXqCGCa9U(8?qy7b$bSTo^Pi1ZZ#74G=Vm#M4!* zGnwXxcs{9{snBZ2nE+(!I;*vj)iRi#GkFSil^ZJMiu8!oT-zArI<$EWATT(~;#i|@ zcfStUJS{akgKjaOU<>{H1*b$SwK^OAC2*JD65YKXE;9fAYYN!ruTPs>y?J?GF3~@K z!VAUtMNtJmxCggKDB@8$aNEiklN zcEaF=6Y(=Ty6)#KJHdQCVGK&GHug6vN`r-FgJQX=Y#UX}q-kt{Fe;fd~&Sa**@!m=k9-Zn-?*4^#2%9nx!vp5m ziJqhijnJs5V$H@_m!oI`zHV(yis0fIZmCS(wJLrH#W~g@gnz<`cJjG3FbB0mB>}q^}UR%0ClCIiJZvbnn+>Fdd@H~3L%(Q zsU0c(EiN1?58UzE|8orX{R&k8UWBUhHkrYLC_32z;d{ML=LhrUuxlNMr9`6CbztFt zbaUqaP=@c@SHcI`vQ#p*vPCr2Bx%T=WZ$!I3E5@I7P5=%B1_2FG9qi3>}1Klk9}Vo z%giv0@5T4^JkQ_oe15%O<2ARr?)$pV^Ei*=jii$E_q>+mnA9D0*}|e(y>evU+D;1@ zjbdS`S5{UAQYTRUs@_hzpKAfAFCjc(LYhya`g(#pO@GMf*xO z)~PhJ52Wd@{hO)t?K)BEtF!95-Q-DPK}lc&FNJO5ZM}uTfdP=Y8Y#1i>?{%Pzj||` zuz)=CKXYXnHt!FzeZ3)%WamrT>ip83h&~BXsm+QEjIQC1CAvuCnQG_w;53*WD`a&h4Oo;Yu6IR>(cc_jjsXSGL1xdxI#jB3jj@z+%M|JYC=IUm`J2He9Tp<8!#% z;P;CasATF#EPe)nPzgRXhC^$u6y z&?Zmb9wg&1Q&WUML@M#tsQ4bNCr(->gFoP;F)!3c>eRXk*uZui#~r&ywiq|lXA_?( zDyPG|jSDn*#tRslL&?Y?tm4zt?>;c^hk%+HBF@5g#pja!!2fkxtwPV7R^-KBM;ynP zSlk`DmT6ieuP&Uu(b81hgQ6_upi;0OFQb`u)2oB9@lHgSSs(}o*puDEJ>a+j*(NiW z%*R$PC_k1j^elH$#G5TDhn;Rb;w{ykZm_@a?RQa-W#kz^hw!tA&I>a$-)~C3t!>+m z?ExpG5XI#J{c?FQMKPV!)zUhyIeNx)$+2m(X0PJy{u~o9Ui%PwA={#)zWe~v0h0U< zng~GwfrE-wSZfb6X5?ll$~P_SV0yao`X4Ef^2kw$%pf-^^K2me{4DcbJ`dr4W;uID z(MBrVMHgq+V>euQeRk)U`$-!xIx`Dsc>Y;b{N+yZ57*G;*e*-KSDulrGccqzQ)@*k ztl+uORsrU%5ALpqm4<{oetE5yHu;+ky2cfE1NZTK$6I5XN5#L#dwV5+DNZ=ZBX4f` zSybZ{1AMSfXU`nQ8*gD^rM!3*6oLH%BL-D|3oCy!&7P#7Ofe@EbYliN=buXBq;5GS z4UnR!uOb*$bH4NZ2KFQ%ulJ7(ES{~xe3jJy37vlB800=*nE&pK>3?N#_W$#LN>+*w z3dMLjb*$aDc9?0v2@44Dzz({SMc6Rn>VSe(l%JnlJXP%sCU8M25aoo##F`mZ;KaZ% z^8NinS?^0w(a>m1SpdDRSRYF`5O?B(MB zbm`KjQI*+XjzSDn)AzZmNDK4)qy7YKFdmD>r~8<&%Gv=Px|Jbc7g>USoi<(X?Scx0 zdt;F^rI`L4KK@5=c=}D^C9svXj};mquqn_S?}JS)ap+kVn@JWFGI>2IpEHPJTwDP| z!GRaE4_6FOQtB_<{+z?mkR`=`wKaBnj!8C$+m-SjgDeoMpYU ze*@Xt1Io)g2P>c@qZPWISfn-bdeZ%*R@>0;uI+v`uyj&VIzGytRsZ&UsnWJ^W2B_} zK}L zKM4VJg8Td7Wme2QDzPA!E-Q6`TJZ+#4jyG=d>jA!kydev^wjWJ9FLMqlcJK6T~6M| z29T^>3=32S9Gpl2{uZN10pmJ69R@UcvKL4yLv;#Mfy~OiHHRcYCu9(78$U>#f!}&FFxes;rUjSEb;Fl0`*~%FPxm7$yzRgq=neL$gPEqQ9uX*qme~LpZS)~e_o%;%6nl^n|5;$bJKk~Gk4>vrdRdbOtiXNnjK4G3)OJFNNA{7y{ zvzxic@O5vcrw|nBIOO{)91_*7OB@Vyh*<09kOO}YsY9}(qirK~lX&n9v~VnVI)w#F z2N0EKUn!@tNxD0Azn%nkvnQVTyEpIWD6ObaV7DA5a-j;xVbVX$8yop@)8T&exq;FQ z!l%!a2%dyqbh~PXPJxCMV3JwO$hb||IJcmrf6T9L(4R>XcniZC9~calDEQmc%S)%g z@UU+#2iX0T#odPoMW^em(&FL<02Lw{A3KyVq?9b8B<`&}G z?i^t6cw)@m#>*n~e$ZR=NpbWBSKIw?*0mTZG45X>AdP;hreBU}?qJaAf?sWy&qT3# zn_;pKeR@Go&9KZli^0KG7Xpg{?sDl%%96f#mD*t8vs*xOq!y7U>05c69j1@DHN}a*?mX zHf^RMeivHGACy}wx6g$f6peqyRVqIM7H=w`}zi-aVCh@ zD3Oe^F_z3)zzhQw7CWR3u6zS zrmo6iU|=Au)(P|C_3I6tJ06~nX2WjNpPH1&nd6a$1}6+RuD&aT2J*l)#CtGL z8TbiZaN8%n=`Toe6j{{KZ1Cvv!b1B97G^Qc2e2VbmHp&q`|}4dxlpOlvP{ygsqvo% zqbkWr!=CQrkm!uf^;_!P#Sj7(k9)9#SwGky4WQ`XiKb>L)IiGwjsyZiO@1HSAuHh7UCmHV! zgqrP8J}s=|#o7Rfr}|z$EqvRRq<_7^4(YgmU?-pU@H;K}HtgZO1bzmkx1jqhF{~e{ zS8!w-LF(eV*GvInC8MiO!uP1o>BkE2KLN>4p0hJI!5qmaQXfmI8P594#Vip|bj7k?KmKayCZPWQ=!DmZ#dQzuMnmIPq zK>cBuR7p`mN7YE6<4D1h&_culQKcKRJK>+=@kK>N7H=#+{ElGeJ|)|{;3UW8q_IhO zzybG*s^GY=oUE>+lfoi@twC&)#z*bf&bt^%uQRMcfpdTA9lF=p=P z&3=F4>g}C@8O41#QE=3_E^Nh;JBKsfeT&`|Yk27=g~~n+jSWIIc=zfy?N|`1LBe1z zFo5_{N1cJP`Z(y!x_t~~>PqlROX*oOa&Yf~peGeGi~YK(<1=O7zX}b69&en_hw(Qq zas`HB)o-e2Lh?eMNj^1d%YBQ^ zE3K;OPiW!Gy=ZUXOvC|W(1F2|fGiQyICc8H*09rjo1+!~DSM@)cT!T&hYvq&nf3i9 z;@?cKZ_PnNyXX=U{=}Vc{~-~tuZUFBskgPXOjmnR3$LsVBFa*@-uJDru@dDx>sC|-WqQe>6xQ_33nFl-`_`LN0Z1_t$iurHJ}c9S~awToEPE-aK#mi}v&2|xiqzNZ3IK; zv0y&cben-(;CO=h`W(xK@~Pguz;_S7!>l1uWxYfEDnpU8u$Ld`i2~@xcGGp&_0MY=)2bGB zcA?VW7yAobV}N_ZtygDxoyG6k?_^fXP5N!b5+);d3 zY^6dp`_5}yaN4m=(_gzgwi^O7oy{^ssH6kNLu&!}0c-}Xmq&9?e+66xa;@fsK8^Bz zg&_;8Sb;%R@25u6?(SM&;;t8gb_RRY#t^H%ITU-doo_7CIQ4x$`yVL~2Hl(X)zm0M zIWNvOZsLS!t;B?AIXc}9*d+a=)yRO;WmVGjKSQw~Se~yKyvtmg@3@C;EJ(&6UwL*D z@t6g?v&9rb#kkxZ?$7*!v;NLeXh{0-@bKCUYGxg_0faluAX$K!&cnXDAkg<-6O3jS zIA+O6$!R{;I{g50+m}MnNzJJE^I7W(OFlJ`AK!djHE- zR!cz1p8n;Ld6VeJI)vm~;CenXI>tR1W3wb;Eu-25A>ELcm*-_O?qfH-6OZZ? zm*r=)|CWJ{jt-EQ=jp!Y>+#;ZcN?DWw{*CZ2P}-5Q-)~|9Bv7I%$`5oQ_svt?j^3K zq}DbP@S$^Pt z_dalO1Kuq`#Z!IW`M(;9a^I9zqLa;YKD-mY#c!a4`SUHmH(gm!S7R>hB4sT(gy{z4 z^M)&O5j~3&1S;hX(=h6KX)>m3WK>p zNDdbac1EKH)e?9_R{ZEFF4C3El&)=~y(1#57-x82S-vXuJX+tFp+S-}32WqJR1t uymQC+=qV`)$?0hzT3Z>A&dJZ(6jGAPbt`A)5DW{rLZT?AB3mMD8u&jT@j+|= literal 0 HcmV?d00001 diff --git a/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-via-url-linux.png b/playwright/snapshots/feedback/rageshakes.spec.ts/rageshake-via-url-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..01a70ee5c42e66c374bd83a2b6709bd048ed40bb GIT binary patch literal 41250 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 literal 0 HcmV?d00001 diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 9d43a13ca4..19bc221444 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -80,7 +80,7 @@ declare global { function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number; interface Window { - mxSendRageshake: (text: string, withLogs?: boolean) => void; + mxSendRageshake: (text: string, withLogs?: boolean) => Promise; matrixLogger: typeof logger; matrixChat?: MatrixChat; mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise; diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index a0ff9f0d6f..8dcf5383eb 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -17,6 +17,12 @@ import { type ValidatedServerConfig } from "./utils/ValidatedServerConfig"; /* eslint-disable camelcase */ /* eslint @typescript-eslint/naming-convention: ["error", { "selector": "property", "format": ["snake_case"] } ] */ +/** + * Bug reports are enabled but must only be locally + * downloadable. + */ +export const BugReportEndpointURLLocal = "local"; + // see element-web config.md for non-developer docs export interface IConfigOptions { // dev note: while true that this is arbitrary JSON, it's valuable to enforce that all @@ -98,7 +104,10 @@ export interface IConfigOptions { show_labs_settings: boolean; features?: Record; // - bug_report_endpoint_url?: string; // omission disables bug reporting + /** + * Bug report endpoint URL. "local" means the logs should not be uploaded. + */ + bug_report_endpoint_url?: typeof BugReportEndpointURLLocal | string; // omission disables bug reporting uisi_autorageshake_app?: string; // defaults to "element-auto-uisi" sentry?: { dsn: string; diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 83e6da8dfe..2977570957 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -1,4 +1,5 @@ /* +Copyright 2026 Element Creations Ltd. Copyright 2024 New Vector Ltd. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Copyright 2019 The Matrix.org Foundation C.I.C. @@ -10,7 +11,7 @@ Please see LICENSE files in the repository root for full details. */ import React, { type JSX, type ReactNode } from "react"; -import { Link } from "@vector-im/compound-web"; +import { Link, Text } from "@vector-im/compound-web"; import SdkConfig from "../../../SdkConfig"; import Modal from "../../../Modal"; @@ -26,6 +27,7 @@ import { sendSentryReport } from "../../../sentry"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { getBrowserSupport } from "../../../SupportedBrowser"; +import { BugReportEndpointURLLocal } from "../../../IConfigOptions"; export interface BugReportDialogProps { onFinished: (success: boolean) => void; @@ -48,6 +50,7 @@ interface IState { export default class BugReportDialog extends React.Component { private unmounted: boolean; private issueRef: React.RefObject; + private readonly isLocalOnly: boolean; public constructor(props: BugReportDialogProps) { super(props); @@ -65,6 +68,8 @@ export default class BugReportDialog extends React.Component support === false)) || !getBrowserSupport() ) { - warning = ( -

- {_t("bug_reporting|unsupported_browser")} -

- ); + warning = {_t("bug_reporting|unsupported_browser")}; } return (
{warning} -

{_t("bug_reporting|description")}

-

- - {_t( - "bug_reporting|before_submitting", - {}, - { - a: (sub) => ( - - {sub} - - ), - }, - )} - -

+ {_t("bug_reporting|description")} + {this.isLocalOnly ? ( + <>{this.state.downloadProgress && {this.state.downloadProgress} ...} + ) : ( + <> + + {_t( + "bug_reporting|before_submitting", + {}, + { + a: (sub) => ( + + {sub} + + ), + }, + )} + -
- - {_t("bug_reporting|download_logs")} - - {this.state.downloadProgress && {this.state.downloadProgress} ...} -
+
+ + {_t("bug_reporting|download_logs")} + + {this.state.downloadProgress && {this.state.downloadProgress} ...} +
- - - {progress} - {error} + + + {progress} + {error} + + )}
); diff --git a/src/components/views/dialogs/FeedbackDialog.tsx b/src/components/views/dialogs/FeedbackDialog.tsx index 72321eeba1..40814d615a 100644 --- a/src/components/views/dialogs/FeedbackDialog.tsx +++ b/src/components/views/dialogs/FeedbackDialog.tsx @@ -45,6 +45,7 @@ const FeedbackDialog: React.FC = (props: IProps) => { const onFinished = (sendFeedback: boolean): void => { if (hasFeedback && sendFeedback) { const label = props.feature ? `${props.feature}-feedback` : "feedback"; + // TODO: Handle rejection. submitFeedback(label, comment, canContact); Modal.createDialog(InfoDialog, { diff --git a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx index 3967de7c4d..8df5b8dbb4 100644 --- a/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx +++ b/src/components/views/dialogs/GenericFeatureFeedbackDialog.tsx @@ -38,7 +38,7 @@ const GenericFeatureFeedbackDialog: React.FC = ({ const sendFeedback = async (ok: boolean): Promise => { if (!ok) return onFinished(false); - + // TODO: Handle rejection. submitFeedback(rageshakeLabel, comment, canContact, rageshakeData); onFinished(true); diff --git a/src/components/views/elements/BugReportDialogButton.tsx b/src/components/views/elements/BugReportDialogButton.tsx new file mode 100644 index 0000000000..284501e7b9 --- /dev/null +++ b/src/components/views/elements/BugReportDialogButton.tsx @@ -0,0 +1,43 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { useCallback } from "react"; +import { Button } from "@vector-im/compound-web"; + +import SdkConfig from "../../../SdkConfig"; +import { _t } from "../../../languageHandler"; +import Modal from "../../../Modal"; +import BugReportDialog, { type BugReportDialogProps } from "../dialogs/BugReportDialog"; +import { BugReportEndpointURLLocal } from "../../../IConfigOptions"; + +/** + * Renders a button to open the BugReportDialog *if* the configuration + * supports it. + */ +export function BugReportDialogButton({ + label, + error, +}: Pick): React.ReactElement | null { + const bugReportUrl = SdkConfig.get().bug_report_endpoint_url; + const onClick = useCallback(() => { + Modal.createDialog(BugReportDialog, { + label, + error, + }); + }, [label, error]); + + if (!bugReportUrl) { + return null; + } + return ( + + ); +} diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx index 7fd2ba9712..ae5e3f93bf 100644 --- a/src/components/views/elements/ErrorBoundary.tsx +++ b/src/components/views/elements/ErrorBoundary.tsx @@ -12,10 +12,9 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import PlatformPeg from "../../../PlatformPeg"; -import Modal from "../../../Modal"; import SdkConfig from "../../../SdkConfig"; -import BugReportDialog from "../dialogs/BugReportDialog"; import AccessibleButton from "./AccessibleButton"; +import { BugReportDialogButton } from "./BugReportDialogButton"; interface Props { children: ReactNode; @@ -60,13 +59,6 @@ export default class ErrorBoundary extends React.PureComponent { }); }; - private onBugReport = (): void => { - Modal.createDialog(BugReportDialog, { - label: "react-soft-crash", - error: this.state.error, - }); - }; - public render(): ReactNode { if (this.state.error) { const newIssueUrl = SdkConfig.get().feedback.new_issue_url; @@ -95,9 +87,7 @@ export default class ErrorBoundary extends React.PureComponent {   {_t("bug_reporting|description")}

- - {_t("bug_reporting|submit_debug_logs")} - + ); } diff --git a/src/components/views/messages/TileErrorBoundary.tsx b/src/components/views/messages/TileErrorBoundary.tsx index 2ccac0f288..2d11371166 100644 --- a/src/components/views/messages/TileErrorBoundary.tsx +++ b/src/components/views/messages/TileErrorBoundary.tsx @@ -12,12 +12,11 @@ import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; -import SdkConfig from "../../../SdkConfig"; -import BugReportDialog from "../dialogs/BugReportDialog"; import AccessibleButton from "../elements/AccessibleButton"; import SettingsStore from "../../../settings/SettingsStore"; import ViewSource from "../../structures/ViewSource"; import { type Layout } from "../../../settings/enums/Layout"; +import { BugReportDialogButton } from "../elements/BugReportDialogButton"; interface IProps { mxEvent: MatrixEvent; @@ -42,13 +41,6 @@ export default class TileErrorBoundary extends React.Component { return { error }; } - private onBugReport = (): void => { - Modal.createDialog(BugReportDialog, { - label: "react-soft-crash-tile", - error: this.state.error, - }); - }; - private onViewSource = (): void => { Modal.createDialog( ViewSource, @@ -69,18 +61,6 @@ export default class TileErrorBoundary extends React.Component { mx_EventTile_tileError: true, }; - let submitLogsButton; - if (SdkConfig.get().bug_report_endpoint_url) { - submitLogsButton = ( - <> -   - - {_t("bug_reporting|submit_debug_logs")} - - - ); - } - let viewSourceButton; if (mxEvent && SettingsStore.getValue("developerMode")) { viewSourceButton = ( @@ -99,7 +79,7 @@ export default class TileErrorBoundary extends React.Component { {_t("timeline|error_rendering_message")} {mxEvent && ` (${mxEvent.getType()})`} - {submitLogsButton} + {viewSourceButton} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index ef0d6e57fc..f4f67e57d3 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -13,16 +13,15 @@ import { type EmptyObject } from "matrix-js-sdk/src/matrix"; import AccessibleButton from "../../../elements/AccessibleButton"; import { _t } from "../../../../../languageHandler"; import SdkConfig from "../../../../../SdkConfig"; -import Modal from "../../../../../Modal"; import PlatformPeg from "../../../../../PlatformPeg"; import UpdateCheckButton from "../../UpdateCheckButton"; -import BugReportDialog from "../../../dialogs/BugReportDialog"; import CopyableText from "../../../elements/CopyableText"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection"; import ExternalLink from "../../../elements/ExternalLink"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; +import { BugReportDialogButton } from "../../../elements/BugReportDialogButton"; interface IState { appVersion: string | null; @@ -80,10 +79,6 @@ export default class HelpUserSettingsTab extends React.Component { - Modal.createDialog(BugReportDialog, {}); - }; - private renderLegal(): ReactNode { const tocLinks = SdkConfig.get().terms_and_conditions_links; if (!tocLinks) return null; @@ -231,9 +226,7 @@ export default class HelpUserSettingsTab extends React.Component } > - - {_t("bug_reporting|submit_debug_logs")} - + {_t( "bug_reporting|matrix_security_issue", diff --git a/src/models/Call.ts b/src/models/Call.ts index 39e4aa4b9a..cb9041bfec 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -44,6 +44,7 @@ import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-t import SdkConfig from "../SdkConfig.ts"; import DMRoomMap from "../utils/DMRoomMap.ts"; import { type WidgetMessaging, WidgetMessagingEvent } from "../stores/widgets/WidgetMessaging.ts"; +import { BugReportEndpointURLLocal } from "../IConfigOptions.ts"; const TIMEOUT_MS = 16000; const logger = rootLogger.getChild("models/Call"); @@ -769,7 +770,7 @@ export class ElementCall extends Call { } const rageshakeSubmitUrl = SdkConfig.get("bug_report_endpoint_url"); - if (rageshakeSubmitUrl) { + if (rageshakeSubmitUrl && rageshakeSubmitUrl !== BugReportEndpointURLLocal) { params.append("rageshakeSubmitUrl", rageshakeSubmitUrl); } diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 2815b50a0c..0101686548 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -20,6 +20,8 @@ import * as rageshake from "./rageshake"; import SettingsStore from "../settings/SettingsStore"; import SdkConfig from "../SdkConfig"; import { getServerVersionFromFederationApi } from "../components/views/dialogs/devtools/ServerInfo"; +import type * as Tar from "tar-js"; +import { BugReportEndpointURLLocal } from "../IConfigOptions"; interface IOpts { labels?: string[]; @@ -342,7 +344,7 @@ async function collectLogs( * the server does not respond with an expected body format. */ export default async function sendBugReport(bugReportEndpoint?: string, opts: IOpts = {}): Promise { - if (!bugReportEndpoint) { + if (!bugReportEndpoint || bugReportEndpoint === BugReportEndpointURLLocal) { throw new Error("No bug report endpoint has been set."); } @@ -354,20 +356,12 @@ export default async function sendBugReport(bugReportEndpoint?: string, opts: IO } /** - * Downloads the files from a bug report. This is the same as sendBugReport, - * but instead causes the browser to download the files locally. + * Loads a bug report into a tarball. * - * @param {object} opts optional dictionary of options - * - * @param {string} opts.userText Any additional user input. - * - * @param {boolean} opts.sendLogs True to send logs - * - * @param {function(string)} opts.progressCallback Callback to call with progress updates - * - * @return {Promise} Resolved when the bug report is downloaded (or started). + * @param opts optional dictionary of options + * @return Resolves with a Tarball object. */ -export async function downloadBugReport(opts: IOpts = {}): Promise { +export async function loadBugReport(opts: IOpts = {}): Promise { const Tar = (await import("tar-js")).default; const progressCallback = opts.progressCallback || ((): void => {}); const body = await collectBugReport(opts, false); @@ -391,7 +385,18 @@ export async function downloadBugReport(opts: IOpts = {}): Promise { } } tape.append("issue.txt", metadata); + return tape; +} +/** + * Downloads the files from a bug report. This is the same as sendBugReport, + * but instead causes the browser to download the files locally. + * + * @param opts optional dictionary of options + * @return Resolved when the bug report is downloaded (or started). + */ +export async function downloadBugReport(opts: IOpts = {}): Promise { + const tape = await loadBugReport(opts); // We have to create a new anchor to download if we want a filename. Otherwise we could // just use window.open. const dl = document.createElement("a"); @@ -417,6 +422,10 @@ export async function submitFeedback( canContact = false, extraData: Record = {}, ): Promise { + const bugReportEndpointUrl = SdkConfig.get().bug_report_endpoint_url; + if (!bugReportEndpointUrl || bugReportEndpointUrl === BugReportEndpointURLLocal) { + throw new Error("Bug report URL is not set or local"); + } let version: string | undefined; try { version = await PlatformPeg.get()?.getAppVersion(); @@ -436,11 +445,7 @@ export async function submitFeedback( body.append(k, JSON.stringify(extraData[k])); } - const bugReportEndpointUrl = SdkConfig.get().bug_report_endpoint_url; - - if (bugReportEndpointUrl) { - await submitReport(bugReportEndpointUrl, body, () => {}); - } + await submitReport(bugReportEndpointUrl, body, () => {}); } /** diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 0e1c269f10..e59d935883 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -405,15 +405,14 @@ export const SETTINGS: Settings = {

), - faq: () => - SdkConfig.get().bug_report_endpoint_url && ( - <> -

{_t("labs|video_rooms_faq1_question")}

-

{_t("labs|video_rooms_faq1_answer")}

-

{_t("labs|video_rooms_faq2_question")}

-

{_t("labs|video_rooms_faq2_answer")}

- - ), + faq: () => ( + <> +

{_t("labs|video_rooms_faq1_question")}

+

{_t("labs|video_rooms_faq1_answer")}

+

{_t("labs|video_rooms_faq2_question")}

+

{_t("labs|video_rooms_faq2_answer")}

+ + ), feedbackLabel: "video-room-feedback", feedbackSubheading: _td("labs|video_rooms_feedbackSubheading"), // eslint-disable-next-line @typescript-eslint/no-require-imports diff --git a/src/utils/Feedback.ts b/src/utils/Feedback.ts index 2c2e55fb41..d75e65b75d 100644 --- a/src/utils/Feedback.ts +++ b/src/utils/Feedback.ts @@ -6,10 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { BugReportEndpointURLLocal } from "../IConfigOptions"; import SdkConfig from "../SdkConfig"; import SettingsStore from "../settings/SettingsStore"; import { UIFeature } from "../settings/UIFeature"; export function shouldShowFeedback(): boolean { - return !!SdkConfig.get().bug_report_endpoint_url && SettingsStore.getValue(UIFeature.Feedback); + const url = SdkConfig.get().bug_report_endpoint_url; + return !!url && url !== BugReportEndpointURLLocal && SettingsStore.getValue(UIFeature.Feedback); } diff --git a/src/vector/rageshakesetup.ts b/src/vector/rageshakesetup.ts index ef104e9a5b..395be79243 100644 --- a/src/vector/rageshakesetup.ts +++ b/src/vector/rageshakesetup.ts @@ -22,7 +22,8 @@ import { logger } from "matrix-js-sdk/src/logger"; import * as rageshake from "../rageshake/rageshake"; import SdkConfig from "../SdkConfig"; -import sendBugReport from "../rageshake/submit-rageshake"; +import sendBugReport, { loadBugReport } from "../rageshake/submit-rageshake"; +import { BugReportEndpointURLLocal } from "../IConfigOptions"; export function initRageshake(): Promise { // we manually check persistence for rageshakes ourselves @@ -54,28 +55,40 @@ export function initRageshakeStore(): Promise { return rageshake.tryInitStorage(); } -window.mxSendRageshake = function (text: string, withLogs?: boolean): void { +window.mxSendRageshake = async function (text: string, withLogs = true): Promise { const url = SdkConfig.get().bug_report_endpoint_url; if (!url) { logger.error("Cannot send a rageshake - no bug_report_endpoint_url configured"); return; } - if (withLogs === undefined) withLogs = true; if (!text || !text.trim()) { logger.error("Cannot send a rageshake without a message - please tell us what went wrong"); return; } - sendBugReport(url, { - userText: text, - sendLogs: withLogs, - progressCallback: logger.log.bind(console), - }).then( - () => { + if (url === BugReportEndpointURLLocal) { + try { + const tape = await loadBugReport({ + userText: text, + sendLogs: withLogs, + progressCallback: logger.log.bind(console), + }); + const blob = new Blob([new Uint8Array(tape.out)], { type: "application/gzip" }); + const url = URL.createObjectURL(blob); + logger.log(`Your logs are available at ${url}`); + } catch (err) { + logger.error("Failed to load bug report", err); + } + } else { + try { + await sendBugReport(url, { + userText: text, + sendLogs: withLogs, + progressCallback: logger.log.bind(console), + }); logger.log("Bug report sent!"); - }, - (err) => { - logger.error(err); - }, - ); + } catch (err) { + logger.error("Failed to send bug report", err); + } + } }; diff --git a/test/unit-tests/components/views/dialogs/BugReportDialog-test.tsx b/test/unit-tests/components/views/dialogs/BugReportDialog-test.tsx index 6a8506887e..78257711c2 100644 --- a/test/unit-tests/components/views/dialogs/BugReportDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/BugReportDialog-test.tsx @@ -17,6 +17,7 @@ import BugReportDialog, { import SdkConfig from "../../../../../src/SdkConfig"; import { type ConsoleLogger } from "../../../../../src/rageshake/rageshake"; import SettingsStore from "../../../../../src/settings/SettingsStore"; +import { BugReportEndpointURLLocal } from "../../../../../src/IConfigOptions"; const BUG_REPORT_URL = "https://example.org/submit"; @@ -69,7 +70,7 @@ describe("BugReportDialog", () => { it("can submit a bug report", async () => { const { getByLabelText, getByText } = renderComponent(); - fetchMock.postOnce(BUG_REPORT_URL, { report_url: "https://exmaple.org/report/url" }); + fetchMock.postOnce(BUG_REPORT_URL, { report_url: "https://example.org/report/url" }); await userEvent.type(getByLabelText("GitHub issue"), "https://example.org/some/issue"); await userEvent.type(getByLabelText("Notes"), "Additional text"); await userEvent.click(getByText("Send logs")); @@ -78,6 +79,14 @@ describe("BugReportDialog", () => { expect(fetchMock).toHaveFetched(BUG_REPORT_URL); }); + it("renders when the config only allows local downloads", async () => { + SdkConfig.put({ + bug_report_endpoint_url: BugReportEndpointURLLocal, + }); + const { container } = renderComponent(); + expect(container).toMatchSnapshot("local-bug-reporter"); + }); + it.each([ { errcode: undefined, diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/BugReportDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/BugReportDialog-test.tsx.snap new file mode 100644 index 0000000000..2b8749c863 --- /dev/null +++ b/test/unit-tests/components/views/dialogs/__snapshots__/BugReportDialog-test.tsx.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`BugReportDialog renders when the config only allows local downloads: local-bug-reporter 1`] = ` +
+
+
+ > +

+ How can I create a video room? +

+

+ Use the “+” button in the room section of the left panel. +

+

+ Can I use text chat alongside the video call? +

+

+ Yes, the chat timeline is displayed alongside the video. +

+
{ beforeEach(() => { jest.useFakeTimers(); ({ client, room, alice, roomSession } = setUpClientRoomAndStores()); - SdkConfig.reset(); }); afterEach(() => { jest.runOnlyPendingTimers(); jest.useRealTimers(); + SdkConfig.reset(); cleanUpClientRoomAndStores(client, room); }); @@ -699,6 +700,24 @@ describe("ElementCall", () => { SettingsStore.getValue = originalGetValue; }); + it.each([ + [undefined, null], + [BugReportEndpointURLLocal, null], + ["other-value", "other-value"], + ])("passes rageshake URL through widget URL", async (configSetting, expectedValue) => { + // Test with the preference set to false + SdkConfig.put({ + bug_report_endpoint_url: configSetting, + }); + ElementCall.create(room); + const call1 = Call.get(room); + if (!(call1 instanceof ElementCall)) throw new Error("Failed to create call"); + + const urlParams1 = new URLSearchParams(new URL(call1.widget.url).hash.slice(1)); + expect(urlParams1.get("rageshakeSubmitUrl")).toBe(expectedValue); + call1.destroy(); + }); + it("passes analyticsID and posthog params through widget URL", async () => { SdkConfig.put({ posthog: { diff --git a/test/unit-tests/submit-rageshake-test.ts b/test/unit-tests/submit-rageshake-test.ts index 54392b58e5..e3f623ef6c 100644 --- a/test/unit-tests/submit-rageshake-test.ts +++ b/test/unit-tests/submit-rageshake-test.ts @@ -18,11 +18,13 @@ import { import fetchMock from "@fetch-mock/jest"; import { getMockClientWithEventEmitter, mockClientMethodsCrypto, mockPlatformPeg } from "../test-utils"; -import { collectBugReport } from "../../src/rageshake/submit-rageshake"; +import { collectBugReport, downloadBugReport, submitFeedback } from "../../src/rageshake/submit-rageshake"; import SettingsStore from "../../src/settings/SettingsStore"; import { type ConsoleLogger } from "../../src/rageshake/rageshake"; import { type FeatureSettingKey, type SettingKey } from "../../src/settings/Settings.tsx"; import { SettingLevel } from "../../src/settings/SettingLevel.ts"; +import SdkConfig from "../../src/SdkConfig.ts"; +import { BugReportEndpointURLLocal } from "../../src/IConfigOptions.ts"; describe("Rageshakes", () => { let mockClient: Mocked; @@ -53,6 +55,10 @@ describe("Rageshakes", () => { jest.spyOn(window, "matchMedia").mockReturnValue({ matches: false } as any); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + describe("Basic Information", () => { it("should include app version", async () => { mockPlatformPeg({ getAppVersion: jest.fn().mockReturnValue("1.11.58") }); @@ -493,7 +499,7 @@ describe("Rageshakes", () => { expect(settingsData.showHiddenEventsInTimeline).toEqual(true); }); - it("should collect logs", async () => { + it("should collect logs for collectBugReport", async () => { const mockConsoleLogger = { flush: jest.fn(), consume: jest.fn(), @@ -511,6 +517,37 @@ describe("Rageshakes", () => { } }); + it("should collect logs for downloadBugReport", async () => { + const mockConsoleLogger = { + flush: jest.fn(), + consume: jest.fn(), + warn: jest.fn(), + } as unknown as Mocked; + mockConsoleLogger.flush.mockReturnValue("line 1\nline 2\n"); + + const prevLogger = global.mx_rage_logger; + global.mx_rage_logger = mockConsoleLogger; + const mockElement = { + href: "", + download: "", + click: jest.fn(), + }; + jest.spyOn(document, "createElement").mockReturnValue(mockElement as any); + jest.spyOn(document, "body", "get").mockReturnValue({ + appendChild: jest.fn(), + removeChild: jest.fn(), + } as any); + try { + await downloadBugReport({ sendLogs: true }); + } finally { + global.mx_rage_logger = prevLogger; + } + expect(document.createElement).toHaveBeenCalledWith("a"); + expect(mockElement.href).toMatch(/^data:application\/octet-stream;base64,.+/); + expect(mockElement.download).toEqual("rageshake.tar"); + expect(mockElement.click).toHaveBeenCalledWith(); + }); + it("should notify progress", () => { const progressCallback = jest.fn(); @@ -518,4 +555,22 @@ describe("Rageshakes", () => { expect(progressCallback).toHaveBeenCalled(); }); + + describe("submitFeedback", () => { + afterEach(() => { + SdkConfig.reset(); + }); + it("fails if the URL is not defined", async () => { + SdkConfig.put({ bug_report_endpoint_url: undefined }); + await expect(() => submitFeedback("label", "comment")).rejects.toThrow( + "Bug report URL is not set or local", + ); + }); + it("fails if the URL is 'local'", async () => { + SdkConfig.put({ bug_report_endpoint_url: BugReportEndpointURLLocal }); + await expect(() => submitFeedback("label", "comment")).rejects.toThrow( + "Bug report URL is not set or local", + ); + }); + }); }); diff --git a/test/unit-tests/utils/Feedback-test.ts b/test/unit-tests/utils/Feedback-test.ts index a0e04df2e8..089b1cefc1 100644 --- a/test/unit-tests/utils/Feedback-test.ts +++ b/test/unit-tests/utils/Feedback-test.ts @@ -6,33 +6,54 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { mocked } from "jest-mock"; - import SdkConfig from "../../../src/SdkConfig"; import { shouldShowFeedback } from "../../../src/utils/Feedback"; import SettingsStore from "../../../src/settings/SettingsStore"; +import { UIFeature } from "../../../src/settings/UIFeature"; +import { BugReportEndpointURLLocal } from "../../../src/IConfigOptions"; -jest.mock("../../../src/SdkConfig"); -jest.mock("../../../src/settings/SettingsStore"); +const realGetValue = SettingsStore.getValue; describe("shouldShowFeedback", () => { + afterEach(() => { + SdkConfig.reset(); + jest.restoreAllMocks(); + }); + it("should return false if bug_report_endpoint_url is falsey", () => { - mocked(SdkConfig).get.mockReturnValue({ - bug_report_endpoint_url: null, + SdkConfig.put({ + bug_report_endpoint_url: undefined, }); - expect(shouldShowFeedback()).toBeFalsy(); + expect(shouldShowFeedback()).toEqual(false); + }); + + it("should return false if bug_report_endpoint_url is 'test'", () => { + SdkConfig.put({ + bug_report_endpoint_url: BugReportEndpointURLLocal, + }); + expect(shouldShowFeedback()).toEqual(false); }); it("should return false if UIFeature.Feedback is disabled", () => { - mocked(SettingsStore).getValue.mockReturnValue(false); - expect(shouldShowFeedback()).toBeFalsy(); + jest.spyOn(SettingsStore, "getValue").mockImplementation((key, ...params) => { + if (key === UIFeature.Feedback) { + return false; + } + return realGetValue(key, ...params); + }); + expect(shouldShowFeedback()).toEqual(false); }); it("should return true if bug_report_endpoint_url is set and UIFeature.Feedback is true", () => { - mocked(SdkConfig).get.mockReturnValue({ + SdkConfig.put({ bug_report_endpoint_url: "https://rageshake.server", }); - mocked(SettingsStore).getValue.mockReturnValue(true); - expect(shouldShowFeedback()).toBeTruthy(); + jest.spyOn(SettingsStore, "getValue").mockImplementation((key, ...params) => { + if (key === UIFeature.Feedback) { + return true; + } + return realGetValue(key, ...params); + }); + expect(shouldShowFeedback()).toEqual(true); }); }); diff --git a/test/unit-tests/vector/rageshakesetup-test.ts b/test/unit-tests/vector/rageshakesetup-test.ts new file mode 100644 index 0000000000..81b5064c71 --- /dev/null +++ b/test/unit-tests/vector/rageshakesetup-test.ts @@ -0,0 +1,65 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import fetchMock from "@fetch-mock/jest"; + +import type { Mocked } from "jest-mock"; +import type { ConsoleLogger } from "../../../src/rageshake/rageshake"; +import SdkConfig from "../../../src/SdkConfig"; +import "../../../src/vector/rageshakesetup"; +import { BugReportEndpointURLLocal } from "../../../src/IConfigOptions"; + +const RAGESHAKE_URL = "https://logs.example.org/logtome"; + +describe("mxSendRageshake", () => { + let prevLogger: ConsoleLogger; + beforeEach(() => { + fetchMock.mockGlobal(); + SdkConfig.put({ bug_report_endpoint_url: RAGESHAKE_URL }); + fetchMock.postOnce(RAGESHAKE_URL, { status: 200, body: {} }); + + const mockConsoleLogger = { + flush: jest.fn(), + consume: jest.fn(), + warn: jest.fn(), + } as unknown as Mocked; + prevLogger = global.mx_rage_logger; + mockConsoleLogger.flush.mockReturnValue("line 1\nline 2\n"); + global.mx_rage_logger = mockConsoleLogger; + }); + + afterEach(() => { + global.mx_rage_logger = prevLogger; + jest.restoreAllMocks(); + fetchMock.unmockGlobal(); + SdkConfig.reset(); + }); + + it("Does not send a rageshake if the URL is not configured", async () => { + SdkConfig.put({ bug_report_endpoint_url: undefined }); + await window.mxSendRageshake("test"); + expect(fetchMock).not.toHaveFetched(); + }); + + it.each(["", " ", undefined, null])("Does not send a rageshake if text is '%s'", async (text) => { + await window.mxSendRageshake(text as string); + expect(fetchMock).not.toHaveFetched(); + }); + + it("Sends a rageshake via URL", async () => { + await window.mxSendRageshake("Hello world"); + expect(fetchMock).toHaveFetched(RAGESHAKE_URL); + }); + + it("Provides a rageshake locally", async () => { + SdkConfig.put({ bug_report_endpoint_url: BugReportEndpointURLLocal }); + const urlSpy = jest.spyOn(URL, "createObjectURL"); + await window.mxSendRageshake("Hello world"); + expect(fetchMock).not.toHaveFetched(RAGESHAKE_URL); + expect(urlSpy).toHaveBeenCalledTimes(1); + }); +});