Skip to content
Open
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
122 changes: 122 additions & 0 deletions api/docs-feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'
import { sheets } from '@googleapis/sheets'
import { GoogleAuth } from 'google-auth-library'

interface FeedbackBody {
page_url: string
rating: 'yes' | 'no'
option?: string
reason?: string
}

function stripHtml(text: string): string {
let prev = text
while (true) {
const next = prev.replace(/<[^>]*>/g, '')
if (next === prev) return next
prev = next
}
}

function sanitize(text: string): string {
return stripHtml(text)
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/@/g, '@\u200B')
.replace(/#(\d)/g, '#\u200B$1')
.slice(0, 1000)
.trim()
}

function isValidPageUrl(url: string): boolean {
if (url.startsWith('/')) return /^\/[\w\-./]*$/.test(url)
try {
const parsed = new URL(url)
return parsed.origin === 'https://docs.metamask.io'
} catch {
return false
}
}

function getDeviceType(ua: string): 'mobile' | 'desktop' {
return /Mobile|Android|iPhone|iPad/i.test(ua) ? 'mobile' : 'desktop'
}

const credentials = JSON.parse(
Buffer.from(process.env.GOOGLE_SHEETS_CREDENTIALS!, 'base64').toString()
)

const auth = new GoogleAuth({
credentials,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
})

const sheetsClient = sheets({ version: 'v4', auth })

async function appendToSheet(row: string[]) {
await sheetsClient.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID!,
range: 'Sheet1!A:E',
valueInputOption: 'RAW',
insertDataOption: 'INSERT_ROWS',
requestBody: { values: [row] },
})
}

export default async function handler(req: VercelRequest, res: VercelResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}

if (!req.body || typeof req.body !== 'object') {
return res.status(400).json({ error: 'Invalid or missing JSON body' })
}

const { page_url: pageUrl, rating, option, reason } = req.body as Partial<FeedbackBody>

if (
typeof pageUrl !== 'string' ||
!pageUrl ||
typeof rating !== 'string' ||
!['yes', 'no'].includes(rating)
) {
return res.status(400).json({ error: 'page_url and rating (yes/no) are required' })
}

if (option !== undefined && typeof option !== 'string') {
return res.status(400).json({ error: 'option must be a string' })
}

if (reason !== undefined && typeof reason !== 'string') {
return res.status(400).json({ error: 'reason must be a string' })
}

if (!isValidPageUrl(pageUrl)) {
return res.status(400).json({ error: 'invalid page_url' })
}

const cleanOption = option ? sanitize(option) : ''
const cleanReason = reason ? sanitize(reason) : ''
Comment thread
cursor[bot] marked this conversation as resolved.
const feedbackDetail = cleanReason || cleanOption

if (rating === 'no' && !feedbackDetail) {
return res.status(400).json({ error: 'option or reason is required for negative feedback' })
}
Comment thread
cursor[bot] marked this conversation as resolved.

const ts = new Date().toISOString()

try {
await appendToSheet([
ts,
pageUrl,
rating,
feedbackDetail,
getDeviceType((req.headers['user-agent'] as string) ?? ''),
])
} catch (err) {
console.error('Google Sheets append failed:', err)
return res.status(500).json({ error: 'Failed to save feedback' })
}
Comment thread
cursor[bot] marked this conversation as resolved.

return res.status(200).json({ ok: true })
}
12 changes: 12 additions & 0 deletions docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,18 @@ const config = {
},
],
'./src/plugins/plugin-json-rpc.ts',
function excludeApiPlugin() {
return {
name: 'exclude-api-directory',
configureWebpack() {
return {
module: {
rules: [{ test: /api\/.*\.ts$/, use: 'null-loader' }],
},
}
},
}
},
// Custom Segment plugin for controlled analytics
'./src/plugins/segment',
'./src/plugins/launchdarkly',
Expand Down
Loading
Loading