Skip to content

Load the Contact Us support launcher from the MFE footer - #346

Open
zamanafzal wants to merge 9 commits into
mainfrom
zafzal/13712-mfe-contact-us-launcher
Open

zamanafzal wants to merge 9 commits into
mainfrom
zafzal/13712-mfe-contact-us-launcher

Conversation

@zamanafzal

@zamanafzal zamanafzal commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What are the relevant tickets?

Refs mitodl/hq#14033, under mitodl/hq#13712

Description (What does it do?)

Footer.jsx is copied into every legacy MFE build, which is how the Appzi script reaches
all seven. ContactUsLauncherScript mounts the same way, next to AppziScript: loads
contactUsLauncher.umd.js, calls init() with the support endpoint and, when signed in,
the learner's email, name and username. learn-ai has no session on this cross-origin POST,
so those three are the ticket's only requester identity. No endpoint, no launcher.

The bundle load and init() sit at module scope and the effect has no cleanup, because
FooterSlot renders inside TabPage and the outline and courseware are separate routes —
opening a subsection remounts the footer with no page load. Unmounting there would close an
open card and reset historyCleared, so the next message starts a new backend thread and
files a second ticket. The card lives on document.body, outside React, so surviving the
remount only takes not tearing it down. The rejection is cached too: the bundle 404s until
the learning MFE is promoted, and that should log once, not refetch on every navigation.

Only the learning MFE lists extra_npm_bundles; the other six load its copy over an
absolute URL, since authoring is on studio_domain and a classic <script src> is
no-cors. ^6.38.1 already resolved to 6.39.x, so the bump just makes the floor explicit.

Pairs with mitodl/ol-infrastructure#6549 (endpoint on mitxonline rc only). #363 stacks on
this branch and adds the runtime waffle gate.

Screenshot:

launcher-on 2-card-open 3-request-body

How can this be tested?

uv run pytest tests/core -q — 243 passed, none of it touching the new component; there's
no JS test harness in this repo for the slot files.

A throwaway Node harness covers the remount behaviour: it extracts the
ContactUsLauncherScript slice out of this Footer.jsx and evaluates it against stubbed
useEffect/useLocation/document/fetch, so it runs the real code. 10 green here, 4/10
before the module-level move. Not committed — nothing here to hang a JS test off.

By hand on a local Tutor stack, learn-ai on :8009, as a learner enrolled in
course-v1:test+13+1:

  1. Copy this Footer.jsx and the 6.39.x contactUsLauncher.umd.js into a
    frontend-app-learning checkout (public/static/smoot-design/).
  2. Set CONTACT_US_SUPPORT_API_URL='http://local.openedx.io:8009/http/support_agent/' and
    CONTACT_US_BUNDLE_PATH='/static/smoot-design/contactUsLauncher.umd.js' — the local MFE
    has no /learn/static/ to serve the default from.
  3. Course home → launcher bottom-right, card opens. Send a message: the POST carries
    user_email, user_name, user_username (third screenshot), each omitted rather than
    empty when absent.
  4. Unset the API URL and restart → no bundle request, no launcher.

Additional Context

  • Authoring renders the launcher but can't send. studio.courses.rc.learn.mit.edu
    isn't in learn-ai's QA cors_frontend_origins. The no-cors script still loads so the card
    opens, but the chat POST is preflighted and the preflight has no matching origin. Adding
    the origin is an ol-infrastructure change, not this one.
  • Promote the learning MFE first. The other six load the bundle out of its deploy and
    nothing orders the promotions, so until learning rebuilds, the bundle 404s.
  • Appzi and the launcher both float bottom-right. No collision today — Appzi is
    production-only, #6549 rc-only — but there will be one when production is enabled. Appzi's
    placement lives in their dashboard, so check the overlap on rc first.

Footer.jsx is copied into every legacy MFE build, so a component here reaches
all seven mitxonline MFEs the same way AppziScript already does. The launcher
initialises only when CONTACT_US_SUPPORT_API_URL is set, which ol-infrastructure
sets on rc and production only.

The bundle is loaded from the learning MFE's copy rather than added to each
MFE's own public/ tree. extra_npm_bundles runs `cp package/dist/bundles/*`,
which is 66MB once source maps are counted, and paying that on six more
artifacts to ship one 900KB file is not worth it. A classic script tag makes a
no-cors request, so authoring loading it from the LMS domain is fine -- this is
the same shape as Appzi loading from w.appzi.io.

The smoot-design floor moves to ^6.39.0 because that is where
contactUsLauncher first shipped; 6.38.1 carries no such bundle, and the
learning MFE is now the only copy of it.

Refs hq#13991
The six non-learning MFEs load the bundle out of the learning MFE's deploy,
and nothing orders those deploys, so a 404 window is real.  Without an
onerror handler the launcher just never appeared, with nothing in the
console; `window.contactUsLauncher?.init` swallowed the same way when a
static host rewrote the 404 to index.html, since a classic script that throws
during evaluation still fires `load`.

Also honour CONTACT_US_BUNDLE_PATH, matching AI_DRAWER_BUNDLE_PATH and
FEEDBACK_DRAWER_BUNDLE_PATH, so a local harness can point at its own build.

Refs hq#13991

@agent-optibot agent-optibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds a shared Contact Us launcher to the legacy footer, enabling support chat across MFEs with configured API access and authenticated user context. The launcher dynamically loads the learning MFE’s bundle, initializes it, and cleans up on unmount.

Key Changes

  • Loads and initializes the Contact Us support launcher across legacy MFEs.
  • Passes support API configuration and authenticated user email to chat requests.
  • Upgrades Smoot Design to include the shared launcher bundle.
All Relevant File Changes (click to expand)
  • deployments/mit-ol/mfe_slot_config/legacy/Footer.jsx: Dynamically loads, configures, and unmounts the Contact Us launcher.
  • deployments/mit-ol/mfe_slot_config/legacy/build_config.yaml: Updates Smoot Design dependency to version 6.39.0.

💬 Feedback & Commands

Share feedback on my comments with 👍 👎 — I'll stop raising issues you push back on. Think I missed something? Adjust my sensitivity →

5 things I can do →
  • #optibot review — I'll re-review your latest changes (incremental)
  • #optibot full review — I'll re-review the entire PR from scratch
  • #optibot fix — I'll apply my suggested fixes to your code
  • #optibot compliance — I'll run a compliance check on your changes
  • #optibot release notes — I'll write up release notes for your production release

💡 Tip: Add repo-specific conventions that I'll apply to every review — Review Memory.

  • 🔄 re-review this PR

@zamanafzal
zamanafzal marked this pull request as ready for review October 23, 2026 12:10
Copilot AI balanced review requested due to automatic review settings October 23, 2026 12:10

@agent-optibot agent-optibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds a support contact launcher to the learning platform, making chat available across MFEs with authenticated user context. The launcher loads asynchronously from the learning MFE and initializes chat using configured API and bundle URLs.

Key Changes

  • Adds Contact Us chat launcher to the shared footer across MFEs.
  • Passes authenticated users’ email addresses to support chat requests.
  • Ensures launcher scripts handle loading failures and clean up on unmount.
  • Updates Smoot Design dependency to include the contact launcher bundle.
All Relevant File Changes (click to expand)
  • deployments/mit-ol/mfe_slot_config/legacy/Footer.jsx: Loads, initializes, and removes the Contact Us launcher script.
  • deployments/mit-ol/mfe_slot_config/legacy/build_config.yaml: Requires Smoot Design version 6.39.0 for the launcher bundle.

💬 Feedback & Commands

Share feedback on my comments with 👍 👎 — I'll stop raising issues you push back on. Think I missed something? Adjust my sensitivity →

5 things I can do →
  • #optibot review — I'll re-review your latest changes (incremental)
  • #optibot full review — I'll re-review the entire PR from scratch
  • #optibot fix — I'll apply my suggested fixes to your code
  • #optibot compliance — I'll run a compliance check on your changes
  • #optibot release notes — I'll write up release notes for your production release

💡 Tip: Add repo-specific conventions that I'll apply to every review — Review Memory.

@agent-optibot agent-optibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status: Code Looks Good 👍

No blocking issues found.

📌 Reviewed up to the latest commit · ⏱️ Review completed in 2 minutes

  • 🔄 re-review this PR

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-origin loading and Appzi coexistence need release-candidate validation, and rollout requires promoting the learning MFE first.

No open issues 🎉

What changed in this PR

Adds the Contact Us support launcher to the shared legacy MFE footer, loading its bundle from the learning MFE’s deployment.

Changes:

  • Enables the launcher when CONTACT_US_SUPPORT_API_URL is set and passes the signed-in learner’s email when available.
  • Adds bundle-load error reporting and cleanup, and raises the @mitodl/smoot-design minimum to ^6.39.0.
File Description
deployments/​mit-ol/​mfe_slot_config/​legacy/​Footer.jsx Loads and initializes the optional support launcher alongside Appzi.
deployments/​mit-ol/​mfe_slot_config/​legacy/​build_config.yaml Updates the learning MFE’s shared bundle dependency and documents its use.

Review effort: Balanced

A throwing unmount propagated out of the React cleanup. The two sibling
bundle loaders, AIDrawerManagerSidebar and FeedbackDrawerSlot, already
guard theirs the same way.
@zamanafzal zamanafzal added the Needs Review An open Pull Request that is ready for review label Oct 26, 2026
…rt request

The footer already read authenticatedUser.email; name and preferred_username
come off the same JWT at the same moment and were being dropped. learn-ai has
no session on this cross-origin POST, so these are the only values that can
populate the Zendesk requester fields on a ticket filed from the MFE.
...(userName ? { user_name: userName } : {}),
...(userUsername ? { user_username: userUsername } : {}),
};
launcher = window.contactUsLauncher.init({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ties the launcher's lifetime to the footer, and the learning MFE remounts the footer on every course tab change. On frontend-app-learning master (which mitxonline builds), each tab (OutlineTab, DatesTab, ProgressTab, CoursewareContainer) renders its own TabPage, and TabPage renders <FooterSlot />. So going from the outline to courseware, or to Dates or Progress, runs this cleanup: unmount() destroys the open card and its conversation, and the bundle is evaluated again and init() runs again.

The card is deliberately non-modal so learners can keep using the page, which makes navigating mid-conversation a supported case. A learner who opens Contact Us on a broken unit and then checks another tab loses the conversation. Their next send also has clear_history: true, which starts a new thread and can mean a second ticket. This also misses hq#14033's "one launcher per page load".

Suggestion: load the bundle and call init() once per page, outside the component lifecycle, e.g. with a module-level promise like loadPromise in feedbackBundle.js. Then don't unmount on footer cleanup.

@zamanafzal zamanafzal Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed as you suggested: module-level cached promise, no effect cleanup. The card mounts onto document.body, outside React's tree, so surviving the remount only takes not tearing it down.

One correction to the mechanism: the tab bar isn't client-side. ResponsiveCourseTabs.jsx:118-124 and upstream CourseTabLink.tsx:13-19 are plain <a href>, so a tab click is a full page load. The remount that bites is outline ↔ courseware — separate <Route> entries, crossed by SequenceTitle.tsx:27 and CourseBreadcrumbs.jsx:68.

The rejection is cached too, unlike feedbackBundle.js: that retries on a click, this would re-log on every navigation for a failure that won't fix itself.

// learn-ai gets no session on this cross-origin POST, so the ticket's
// requester fields are whatever we send here or nothing.
const requestBody = {
...(userEmail ? { user_email: userEmail } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker for this PR: as the comment above says, learn-ai has no session here, so this client-supplied user_email is the requester's entire identity. Anyone can POST to support_agent/ with any address and file a ticket as that person. Is that limited on the learn-ai or Zendesk side (e.g. where replies go)?

@zamanafzal zamanafzal Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the requester proper — zendesk.py:314 sets "requester": {"email": …} — so a public agent reply does go to that address.

What limits it, all learn-ai side:

  • A signed-in learner can't be renamed by the body: consumers.py:936-960 prefers the session email.
  • An unverified address is marked rather than trusted — tim_unverified_requester tag, subject replaced, first comment filed private (zendesk.py:296-299, :312-313) — so filing doesn't mail the submitted text to a spoofed address. That was mitodl/hq#13985, closed by learn-ai#132.

What isn't limited is the rate: the anonymous throttle key re-derives on every request under signed_cookies (mitodl/hq#13968, open).

Agreed it's not a blocker here — the footer only passes values the backend already accepts. I didn't re-check whether Zendesk's "Verify anonymous requests" is on for the live instance, or how it notifies when the first comment is private.

The learning MFE renders FooterSlot inside each tab's TabPage, and Outline,
Dates, Progress and courseware each render their own TabPage under a separate
route. A tab change therefore remounted the footer and ran the effect cleanup,
which unmounted the launcher: an open card closed, the conversation went with
it, and since historyCleared resets on a fresh init the next message carried
clear_history: true, starting a new thread and filing a second ticket for one
learner's problem. init() has no idempotency guard either, so the bundle was
re-evaluated and re-initialised every time.

Load the bundle and call init() behind a module-level promise instead, and drop
the cleanup. The card mounts onto document.body, outside React's tree, so
surviving the footer unmount takes nothing but not tearing it down. That is
hq#14033's "one launcher per page load".

The rejection is cached as well. Every remount used to retry, and the six
non-learning MFEs 404 until the learning MFE is promoted, so retrying turned a
known-bad bundle into one request per tab change. Report it once instead.

init() is wrapped because a synchronous throw would otherwise escape the load
handler and leave the promise pending, with nothing saying why no launcher
appeared.

Refs hq#14033
init() can throw a non-Error, in which case `.message` is undefined and the
console line says nothing. Passing the value through keeps the stack, which is
the only clue when the throw comes from inside the bundle.

Refs hq#14033
The comment blamed course tab changes, but the tab bar is plain <a href>
(CourseTabLink.tsx:13-19), so those are full page loads and clear module state
anyway. The real client-side remounts cross the outline/courseware route
boundary: SequenceTitle.tsx:27 opening a subsection, and CourseBreadcrumbs.jsx:68
going back to the outline. Both render their own TabPage, which is where
FooterSlot lives (TabPage.tsx:157).

Also record why this promise keeps its rejection where the sibling
feedbackBundle.js discards it.

Refs hq#14033
zamanafzal added a commit that referenced this pull request Oct 30, 2026
#346 moved the launcher to a module-level promise so a footer remount stops
tearing it down. That is incompatible with the gate as written here, which kept
`enabled` in component state: every remount reset it to null, unmounted the
launcher, re-fetched the gate and re-injected the bundle. feoh flagged exactly
this on both PRs.

Resolved by hoisting the gate to module scope alongside the launcher:

- The gate answer is cached per course key, so a remount reuses it instead of
  hiding the launcher and asking again. A different course still re-asks, and
  the launcher stays up until that answer lands rather than flickering.
- Bundle loading is now separate from mounting, so the kill switch can take the
  launcher down and a re-enable brings it back without refetching the bundle.
- `launcherWanted` records the desired state, because the mount awaits the
  bundle and a flag flip can land mid-load.
- `init()` throwing is reported instead of vanishing into a settled promise.

Verified with /tmp/sdd363/gate.mjs, which extracts the real slice of Footer.jsx
and evaluates it, with a React stub that honours both the dependency array and
useState. Against the pre-merge gate: 13 passed, 5 failed. After: 18, 0.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

There are no active review issues for the shared-footer Contact Us launcher change.

No open issues 🎉

Review effort: Balanced

@blarghmatey
blarghmatey added this pull request to stack #365 October 30, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Review An open Pull Request that is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants