Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
106 changes: 106 additions & 0 deletions api/docs-feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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'
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, reason } = req.body as FeedbackBody
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

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

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

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

if (rating === 'no' && !cleanReason) {
return res.status(400).json({ error: '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,
cleanReason,
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