Skip to content

Commit c1e9a98

Browse files
committed
Improve support contact
1 parent 4853b48 commit c1e9a98

8 files changed

Lines changed: 277 additions & 54 deletions

File tree

astra_app/core/templates/core/base.html

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -417,19 +417,13 @@
417417
<a class="text-muted" href="{% url 'privacy-policy' %}">Privacy Policy</a>
418418
{% if request.user.is_authenticated %}
419419
<span class="mx-2">·</span>
420-
<a class="text-muted" href="mailto:{{ default_from_email_address }}">Contact Support</a>
420+
<a class="text-muted" href="mailto:{{ default_from_email_address }}" data-sentry-feedback-link="">Contact Support</a>
421+
{% else %}
422+
<span class="d-none" data-sentry-feedback-footer="" data-sentry-feedback-hidden="true">
423+
<span class="mx-2" aria-hidden="true">·</span>
424+
<button type="button" class="btn btn-link text-muted p-0 border-0 align-baseline" data-sentry-feedback-link="">Contact Support</button>
425+
</span>
421426
{% endif %}
422-
<span class="d-none" data-sentry-feedback-footer="" data-sentry-feedback-hidden="true">
423-
<span class="mx-2" aria-hidden="true">·</span>
424-
<a
425-
class="text-muted"
426-
href="#"
427-
data-sentry-feedback-link=""
428-
data-sentry-feedback-hidden="true"
429-
aria-hidden="true"
430-
tabindex="-1"
431-
>Report a bug</a>
432-
</span>
433427
<span class="mx-2">·</span>
434428
Powered by AlmaLinux Astra
435429
{% if build_sha %}

astra_app/core/tests/test_auth_pages_redirect_logged_in_users.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,22 @@ def test_login_redirects_logged_in_user_to_profile(self) -> None:
4343
self.assertEqual(resp.redirect_chain[-1], (profile_url, 302))
4444
self.assertContains(resp, "Contact Support")
4545
self.assertContains(resp, "mailto:support-test@example.com")
46+
self.assertNotContains(resp, ">Support<", html=False)
47+
self.assertNotContains(resp, "Report a bug")
4648

47-
def test_login_page_hides_footer_support_link_for_anonymous_user(self) -> None:
49+
@override_settings(DEFAULT_FROM_EMAIL="Astra Support <support-test@example.com>")
50+
def test_login_page_footer_support_link_hides_email_for_anonymous_user(self) -> None:
4851
response = self.client.get(reverse("login"))
4952

5053
self.assertEqual(response.status_code, 200)
51-
self.assertNotContains(response, "Contact Support")
52-
self.assertNotContains(response, "mailto:astra@almalinux.org")
54+
self.assertContains(response, 'data-sentry-feedback-footer=""')
55+
self.assertContains(response, 'data-sentry-feedback-hidden="true"')
56+
self.assertContains(response, 'data-sentry-feedback-link=""')
57+
self.assertContains(response, "Contact Support")
58+
self.assertNotContains(response, ">Support<", html=False)
59+
self.assertNotContains(response, "mailto:support-test@example.com")
60+
self.assertNotContains(response, "support-test@example.com")
61+
self.assertNotContains(response, "Report a bug")
5362

5463
def test_password_expired_redirects_logged_in_user(self) -> None:
5564
client = Client()

astra_app/core/tests/test_sentry_browser_templates.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,22 @@ def test_profile_page_includes_sentry_browser_bundle_and_tunnel_config(self) ->
119119
self.assertContains(response, '"tracesSampleRate": 0.25')
120120
self.assertContains(response, '"tunnel": "/_ci/envelope/"')
121121
self.assertContains(response, 'data-sentry-feedback-link=""')
122+
self.assertContains(response, '>Contact Support<', html=False)
123+
self.assertContains(response, 'href="mailto:astra@almalinux.org"', html=False)
124+
self.assertNotContains(response, '>Support<', html=False)
125+
self.assertNotContains(response, 'Report a bug')
126+
127+
@override_settings(DEFAULT_FROM_EMAIL="Astra Support <support-test@example.com>")
128+
def test_login_page_support_link_omits_mailto_email_for_anonymous_user(self) -> None:
129+
response = self.client.get("/login/")
130+
131+
self.assertEqual(response.status_code, 200)
122132
self.assertContains(response, 'data-sentry-feedback-footer=""')
123-
self.assertContains(response, '>Report a bug<', html=False)
124133
self.assertContains(response, 'data-sentry-feedback-hidden="true"')
134+
self.assertContains(response, 'data-sentry-feedback-link=""')
135+
self.assertContains(response, '>Contact Support<', html=False)
136+
self.assertNotContains(response, 'href="mailto:support-test@example.com"', html=False)
137+
self.assertNotContains(response, 'support-test@example.com')
125138

126139

127140
class SentryBlockedTemplateMarkerTests(SimpleTestCase):

frontend/src/elections/__tests__/electionsEntry.test.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,10 @@ describe("mountElectionsPage", () => {
2222
const footer = document.createElement("footer");
2323
footer.innerHTML = `
2424
<a
25-
class="text-muted d-none"
26-
href="#"
25+
class="text-muted"
26+
href="mailto:support@example.com"
2727
data-sentry-feedback-link=""
28-
data-sentry-feedback-hidden="true"
29-
aria-hidden="true"
30-
tabindex="-1"
31-
>Report a bug</a>
28+
>Support</a>
3229
`;
3330
document.body.appendChild(footer);
3431
return footer.querySelector("[data-sentry-feedback-link]") as HTMLAnchorElement;
@@ -77,8 +74,8 @@ describe("mountElectionsPage", () => {
7774
expect(app).not.toBeNull();
7875
expect(root.querySelector("[data-elections-vue-root]")).not.toBeNull();
7976
expect(root.querySelector("[data-sentry-feedback-trigger]")).toBeNull();
80-
expect(footerLink.textContent).toBe("Report a bug");
81-
expect(footerLink.classList.contains("d-none")).toBe(false);
77+
expect(footerLink.textContent).toBe("Support");
78+
expect(footerLink.getAttribute("href")).toBe("mailto:support@example.com");
8279
expect(attachTo).toHaveBeenCalledTimes(1);
8380
expect(attachTo).toHaveBeenCalledWith(
8481
footerLink,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
3+
const attachSentryFeedbackTrigger = vi.fn();
4+
5+
vi.mock("../../shared/sentryFeedback", () => ({
6+
attachSentryFeedbackTrigger,
7+
}));
8+
9+
describe("sentryBrowser entrypoint", () => {
10+
afterEach(() => {
11+
document.head.innerHTML = "";
12+
document.body.innerHTML = "";
13+
attachSentryFeedbackTrigger.mockReset();
14+
vi.restoreAllMocks();
15+
vi.resetModules();
16+
});
17+
18+
it("binds the shared footer support control globally on non-blocked pages", async () => {
19+
document.body.innerHTML = `
20+
<footer>
21+
<a href="mailto:support@example.com" data-sentry-feedback-link="">Contact Support</a>
22+
</footer>
23+
`;
24+
25+
await import("../sentryBrowser");
26+
document.dispatchEvent(new Event("DOMContentLoaded"));
27+
28+
expect(attachSentryFeedbackTrigger).toHaveBeenCalledTimes(1);
29+
expect(attachSentryFeedbackTrigger).toHaveBeenCalledWith(
30+
document.body,
31+
{
32+
allowScreenshot: true,
33+
surface: "global-footer",
34+
},
35+
);
36+
});
37+
38+
it("does not bind the shared footer support control on blocked pages", async () => {
39+
document.head.innerHTML = '<meta name="sentry-capture-disabled" content="true">';
40+
document.body.innerHTML = `
41+
<footer>
42+
<a href="mailto:support@example.com" data-sentry-feedback-link="">Contact Support</a>
43+
</footer>
44+
`;
45+
46+
await import("../sentryBrowser");
47+
document.dispatchEvent(new Event("DOMContentLoaded"));
48+
49+
expect(attachSentryFeedbackTrigger).not.toHaveBeenCalled();
50+
});
51+
});

frontend/src/entrypoints/sentryBrowser.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import "vite/modulepreload-polyfill";
22

33
import * as SentryBrowser from "@sentry/browser";
44

5+
import { attachSentryFeedbackTrigger } from "../shared/sentryFeedback";
6+
57
declare global {
68
interface Window {
79
Sentry?: typeof SentryBrowser;
@@ -10,4 +12,23 @@ declare global {
1012

1113
window.Sentry = SentryBrowser;
1214

15+
function bindGlobalFooterFeedback(): void {
16+
if (document.querySelector('meta[name="sentry-capture-disabled"][content="true"]') !== null) {
17+
return;
18+
}
19+
20+
attachSentryFeedbackTrigger(document.body, {
21+
allowScreenshot: true,
22+
surface: "global-footer",
23+
});
24+
}
25+
26+
if (document.readyState === "complete") {
27+
bindGlobalFooterFeedback();
28+
} else {
29+
document.addEventListener("DOMContentLoaded", () => {
30+
bindGlobalFooterFeedback();
31+
}, { once: true });
32+
}
33+
1334
export {};

frontend/src/shared/__tests__/sentryFeedback.test.ts

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,47 @@ function buildRoot(): HTMLDivElement {
88
return root;
99
}
1010

11-
function buildFooterLink(): HTMLAnchorElement {
11+
function buildFooterLink(): HTMLElement {
12+
const footer = document.createElement("footer");
13+
footer.innerHTML = `
14+
<a
15+
class="text-muted"
16+
href="mailto:support@example.com"
17+
data-sentry-feedback-link=""
18+
>Contact Support</a>
19+
<span class="mx-2">·</span>
20+
`;
21+
document.body.appendChild(footer);
22+
return footer.querySelector("[data-sentry-feedback-link]") as HTMLElement;
23+
}
24+
25+
function buildHiddenAnonymousFooterButton(): HTMLElement {
1226
const footer = document.createElement("footer");
1327
footer.innerHTML = `
1428
<span class="d-none" data-sentry-feedback-footer="" data-sentry-feedback-hidden="true">
15-
<span class="mx-2">·</span>
16-
<a
17-
class="text-muted"
18-
href="#"
29+
<button
30+
type="button"
31+
class="btn btn-link text-muted p-0 border-0 align-baseline"
1932
data-sentry-feedback-link=""
20-
data-sentry-feedback-hidden="true"
21-
aria-hidden="true"
22-
tabindex="-1"
23-
>Report a bug</a>
33+
>Contact Support</button>
2434
</span>
25-
<span class="mx-2">·</span>
2635
`;
2736
document.body.appendChild(footer);
28-
return footer.querySelector("[data-sentry-feedback-link]") as HTMLAnchorElement;
37+
return footer.querySelector("[data-sentry-feedback-link]") as HTMLElement;
38+
}
39+
40+
function buildFeedbackShadowForm(): ShadowRoot {
41+
const host = document.createElement("div");
42+
document.body.appendChild(host);
43+
const shadowRoot = host.attachShadow({ mode: "open" });
44+
shadowRoot.innerHTML = `
45+
<div class="dialog__content">
46+
<form class="form">
47+
<fieldset class="form__right" data-sentry-feedback="true"></fieldset>
48+
</form>
49+
</div>
50+
`;
51+
return shadowRoot;
2952
}
3053

3154
describe("attachSentryFeedbackTrigger", () => {
@@ -35,7 +58,7 @@ describe("attachSentryFeedbackTrigger", () => {
3558
vi.restoreAllMocks();
3659
});
3760

38-
it("attaches a report issue trigger on eligible roots and enables screenshots only when allowed", () => {
61+
it("repurposes the shared contact-support link on eligible roots and injects a modal fallback email link", () => {
3962
const attachTo = vi.fn();
4063
vi.stubGlobal("window", window);
4164
vi.stubGlobal("Sentry", {
@@ -47,7 +70,6 @@ describe("attachSentryFeedbackTrigger", () => {
4770

4871
const root = buildRoot();
4972
const footerLink = buildFooterLink();
50-
const footerWrapper = document.querySelector("[data-sentry-feedback-footer]") as HTMLSpanElement;
5173

5274
const attached = attachSentryFeedbackTrigger(root, {
5375
allowScreenshot: true,
@@ -56,23 +78,76 @@ describe("attachSentryFeedbackTrigger", () => {
5678

5779
expect(attached).toBe(true);
5880
expect(root.querySelector("[data-sentry-feedback-trigger]" )).toBeNull();
59-
expect(footerLink.textContent).toBe("Report a bug");
60-
expect(footerWrapper.classList.contains("d-none")).toBe(false);
61-
expect(footerWrapper.getAttribute("data-sentry-feedback-hidden")).toBe("false");
62-
expect(footerLink.getAttribute("data-sentry-feedback-hidden")).toBe("false");
63-
expect(footerLink.hasAttribute("aria-hidden")).toBe(false);
64-
expect(footerLink.hasAttribute("tabindex")).toBe(false);
81+
expect(footerLink.textContent).toBe("Contact Support");
82+
expect(footerLink.getAttribute("href")).toBe("mailto:support@example.com");
83+
84+
const clickEvent = new MouseEvent("click", { bubbles: true, cancelable: true });
85+
footerLink.dispatchEvent(clickEvent);
86+
expect(clickEvent.defaultPrevented).toBe(true);
87+
6588
expect(attachTo).toHaveBeenCalledTimes(1);
6689
expect(attachTo).toHaveBeenCalledWith(
6790
footerLink,
6891
expect.objectContaining({
6992
enableScreenshot: true,
93+
onFormOpen: expect.any(Function),
7094
showBranding: false,
7195
tags: expect.objectContaining({
7296
feedback_surface: "groups-detail",
7397
}),
7498
}),
7599
);
100+
101+
const attachOptions = attachTo.mock.calls[0]?.[1];
102+
expect(attachOptions).toBeDefined();
103+
104+
const shadowRoot = buildFeedbackShadowForm();
105+
attachOptions.onFormOpen();
106+
107+
const fallback = shadowRoot.querySelector("[data-astra-support-fallback]") as HTMLParagraphElement | null;
108+
expect(fallback).not.toBeNull();
109+
expect(fallback?.textContent).toContain("Prefer email?");
110+
111+
const fallbackLink = shadowRoot.querySelector("[data-astra-support-fallback-link]") as HTMLAnchorElement | null;
112+
expect(fallbackLink?.textContent).toBe("Email support");
113+
expect(fallbackLink?.getAttribute("href")).toBe("mailto:support@example.com");
114+
115+
attachOptions.onFormOpen();
116+
expect(shadowRoot.querySelectorAll("[data-astra-support-fallback]")).toHaveLength(1);
117+
});
118+
119+
it("reveals the hidden anonymous contact-support scaffold only after feedback binds", () => {
120+
const attachTo = vi.fn();
121+
(window as typeof window & { Sentry?: unknown }).Sentry = {
122+
getFeedback: () => ({ attachTo }),
123+
};
124+
125+
const root = buildRoot();
126+
const footerLink = buildHiddenAnonymousFooterButton();
127+
const footerWrapper = document.querySelector("[data-sentry-feedback-footer]") as HTMLElement;
128+
129+
expect(footerWrapper.classList.contains("d-none")).toBe(true);
130+
expect(footerWrapper.getAttribute("data-sentry-feedback-hidden")).toBe("true");
131+
132+
const attached = attachSentryFeedbackTrigger(root, {
133+
allowScreenshot: false,
134+
surface: "anonymous-groups",
135+
});
136+
137+
expect(attached).toBe(true);
138+
expect(footerLink.textContent).toBe("Contact Support");
139+
expect(footerLink.getAttribute("href")).toBeNull();
140+
expect(footerWrapper.classList.contains("d-none")).toBe(false);
141+
expect(footerWrapper.getAttribute("data-sentry-feedback-hidden")).toBe("false");
142+
expect(attachTo).toHaveBeenCalledWith(
143+
footerLink,
144+
expect.objectContaining({
145+
enableScreenshot: false,
146+
tags: expect.objectContaining({
147+
feedback_surface: "anonymous-groups",
148+
}),
149+
}),
150+
);
76151
});
77152

78153
it("does not attach feedback on blocked roots", () => {
@@ -83,7 +158,6 @@ describe("attachSentryFeedbackTrigger", () => {
83158

84159
const root = buildRoot();
85160
const footerLink = buildFooterLink();
86-
const footerWrapper = document.querySelector("[data-sentry-feedback-footer]") as HTMLSpanElement;
87161
root.setAttribute("data-sentry-capture-disabled", "");
88162

89163
const attached = attachSentryFeedbackTrigger(root, {
@@ -93,9 +167,11 @@ describe("attachSentryFeedbackTrigger", () => {
93167

94168
expect(attached).toBe(false);
95169
expect(root.querySelector("[data-sentry-feedback-trigger]")).toBeNull();
96-
expect(footerWrapper.classList.contains("d-none")).toBe(true);
97-
expect(footerWrapper.getAttribute("data-sentry-feedback-hidden")).toBe("true");
98-
expect(footerLink.getAttribute("data-sentry-feedback-hidden")).toBe("true");
170+
expect(footerLink.getAttribute("href")).toBe("mailto:support@example.com");
171+
172+
const clickEvent = new MouseEvent("click", { bubbles: true, cancelable: true });
173+
footerLink.dispatchEvent(clickEvent);
174+
expect(clickEvent.defaultPrevented).toBe(false);
99175
expect(attachTo).not.toHaveBeenCalled();
100176
});
101177

@@ -111,7 +187,6 @@ describe("attachSentryFeedbackTrigger", () => {
111187

112188
const root = buildRoot();
113189
const footerLink = buildFooterLink();
114-
const footerWrapper = document.querySelector("[data-sentry-feedback-footer]") as HTMLSpanElement;
115190
blockedBoundary.appendChild(root);
116191

117192
const attached = attachSentryFeedbackTrigger(root, {
@@ -121,8 +196,7 @@ describe("attachSentryFeedbackTrigger", () => {
121196

122197
expect(attached).toBe(false);
123198
expect(root.querySelector("[data-sentry-feedback-trigger]")).toBeNull();
124-
expect(footerWrapper.classList.contains("d-none")).toBe(true);
125-
expect(footerLink.getAttribute("data-sentry-feedback-hidden")).toBe("true");
199+
expect(footerLink.getAttribute("href")).toBe("mailto:support@example.com");
126200
expect(attachTo).not.toHaveBeenCalled();
127201
});
128202

0 commit comments

Comments
 (0)