Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion main/ui/auth0-docs-ui-1.1.0.css

This file was deleted.

1 change: 1 addition & 0 deletions main/ui/auth0-docs-ui-1.2.0.css

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ui",
"private": true,
"version": "1.1.0",
"version": "1.2.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
6 changes: 5 additions & 1 deletion ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createRoot } from 'react-dom/client';
import './index.css';

import { NavActions } from '@/components';
import { initFeedbackListeners } from '@/lib/feedback';
import { overrideHistoryMethods } from '@/lib/history';
import { initOneTrust } from '@/lib/one-trust';
import { initRootStore } from '@/stores';
Expand Down Expand Up @@ -36,6 +37,9 @@ async function main() {
// Initialize one-trust for cookie-consent management
initOneTrust();

// Initialize feedback listeners for Mintlify documentation pages
initFeedbackListeners();

// Mount the main application
mountApp(root);
}
Expand All @@ -49,4 +53,4 @@ export * from '@/components';
export { initRootStore, rootStore } from '@/stores';
export { autorun, observe, reaction } from 'mobx';
export { observer } from 'mobx-react-lite';
export { getSample, postSample } from '@/lib/api';
export { getSample, postSample, postFeedback } from '@/lib/api';
16 changes: 16 additions & 0 deletions ui/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,22 @@ export async function patchRolloutConsent(consentData: RolloutConsentRequest) {
);
}

// Feedback Interfaces
export interface FeedbackRequest {
positive: boolean;
page_url: string;
page_title: string;
comment: string;
}

// Feedback Methods
export async function postFeedback(feedbackData: FeedbackRequest) {
return request<FeedbackRequest>(`${config.apiBaseUrl}/feedback`, {
method: 'POST',
body: JSON.stringify(feedbackData),
});
}

// Sample Methods
export async function getSample(
params: {
Expand Down
229 changes: 229 additions & 0 deletions ui/src/lib/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { postFeedback } from './api';

/**
* Feedback listeners for Mintlify documentation pages
*
* This module sets up event listeners to capture user feedback from:
* 1. Page-level feedback forms (at the bottom of each documentation page)
* 2. Code snippet feedback forms (associated with code blocks)
*
* The implementation uses event delegation on the document body to handle
* dynamically rendered Mintlify components.
*/

type FeedbackMode = 'POSITIVE' | 'NEGATIVE';

/**
* Determines the tracking code based on the current page URL
*/
function getTrackingCode(): string {
const path = window.location.pathname;

if (path.startsWith('/docs/api/management')) {
return 'docs:management-api';
}

if (path.startsWith('/docs/api/authentication')) {
return 'docs:auth-api';
}

if (path.startsWith('/docs/quickstarts')) {
return 'quickstarts:code-snippet';
}

return '';
}

/**
* Determines the feedback mode (POSITIVE or NEGATIVE) for page-level feedback
* by checking which button has the "border" class.
*
* IMPORTANT: This is fragile code that relies on Mintlify's current HTML structure.
* TODO: Request Mintlify to add unique identifiers to these buttons.
*/
function getFeedbackMode(): FeedbackMode {
// Query select all buttons inside the feedback-toolbar but not inside contextual-feedback-container
const toolbar = document.querySelector('.feedback-toolbar');
if (!toolbar) return 'NEGATIVE'; // Default to negative if we can't determine

const feedbackContainer = toolbar.querySelector('.contextual-feedback-container');
const buttons = Array.from(toolbar.querySelectorAll('button')).filter(
(button) => !feedbackContainer?.contains(button),
);

// The first button is for positive feedback, the second is for negative
// Check which one has the "border" class (indicates it was clicked)
if (buttons.length >= 2) {
const firstButton = buttons[0];
const secondButton = buttons[1];

if (firstButton.classList.contains('border')) {
return 'POSITIVE';
}

if (secondButton.classList.contains('border')) {
return 'NEGATIVE';
}
}

// Default to NEGATIVE if we can't determine
return 'NEGATIVE';
}

/**
* Tracks feedback events in Heap Analytics
*/
function trackFeedback(
feedbackMode: FeedbackMode,
message: string,
eventComponent: string,
) {
if (!window.heap) {
console.warn('Heap analytics not available');
return;
}

const trackingCode = getTrackingCode();

// Track the comment submission
window.heap.track('submit:auth0-docs:feedback-comment', {
value: feedbackMode,
comment: message,
component: eventComponent,
dwh: {
event: `submit:${trackingCode}:feedback-message`,
properties: {
trackData: message,
track_data2: feedbackMode,
track_data3: eventComponent,
},
},
});
}

/**
* Tracks boolean feedback (thumbs up/down) in Heap Analytics
*/
function trackBooleanFeedback(feedbackMode: FeedbackMode) {
if (!window.heap) {
console.warn('Heap analytics not available');
return;
}

const trackingCode = getTrackingCode();
// TODO: Handle ApiExplorerFooter component tracking later
const eventComponent = '';

window.heap.track('click:auth0-docs:feedback-boolean', {
value: feedbackMode,
component: eventComponent,
dwh: {
event: `submit:${trackingCode}:feedback-helpful`,
properties: {
trackData: feedbackMode === 'POSITIVE',
track_data2: eventComponent,
},
},
});
}

/**
* Handles page-level feedback form submission
*/
async function handlePageFeedbackSubmit(event: Event) {
event.preventDefault();

const form = event.target as HTMLFormElement;
const input = form.querySelector('.contextual-feedback-input') as
| HTMLInputElement
| HTMLTextAreaElement;

if (!input || !input.value.trim()) {
return;
}

const feedbackMode = getFeedbackMode();
const message = input.value.trim();

// Track in analytics
trackFeedback(feedbackMode, message, 'FeedbackSection');
trackBooleanFeedback(feedbackMode);

// Submit to API
try {
await postFeedback({
positive: feedbackMode === 'POSITIVE',
page_url: window.location.href,
page_title: document.title,
comment: message,
});
console.log('Feedback submitted successfully');
// Optionally show success message to user
} catch (error) {
console.error('Failed to submit feedback:', error);
}
}

/**
* Handles code snippet feedback form submission
*/
async function handleCodeSnippetFeedbackClick(event: Event) {
const button = event.target as HTMLElement;

// Find the closest feedback form container
const feedbackForm = button.closest('.code-snippet-feedback-form');
if (!feedbackForm) return;

const textarea = feedbackForm.querySelector(
'.code-snippet-feedback-textarea',
) as HTMLTextAreaElement;

if (!textarea || !textarea.value.trim()) {
return;
}

const message = textarea.value.trim();
const feedbackMode: FeedbackMode = 'NEGATIVE'; // Code snippet feedback is always negative

// Track in analytics
trackFeedback(feedbackMode, message, 'FeedbackSection');

// Submit to API
try {
await postFeedback({
positive: false,
page_url: window.location.href,
page_title: document.title,
comment: message,
});
console.log('Code snippet feedback submitted successfully');
// Optionally show success message to user
} catch (error) {
console.error('Failed to submit code snippet feedback:', error);
}
}

/**
* Initializes feedback event listeners using event delegation
*/
export function initFeedbackListeners() {
// Listener for page-level feedback form submission
document.body.addEventListener('submit', (event) => {
const form = event.target as HTMLElement;
if (form.classList.contains('contextual-feedback-form')) {
handlePageFeedbackSubmit(event);
}
});

// Listener for code snippet feedback button clicks
document.body.addEventListener('click', (event) => {
const button = event.target as HTMLElement;
if (
button.classList.contains('code-snippet-feedback-form-submit-button')
) {
handleCodeSnippetFeedbackClick(event);
}
});

console.log('Feedback listeners initialized');
}