This commit is contained in:
2026-02-04 12:51:41 +00:00
parent 4fdbfb0fb3
commit f1e13f87f6
19 changed files with 722 additions and 67 deletions

View File

@@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'
import { isAuthenticatedNextjs } from "@convex-dev/auth/nextjs/server";
import { z } from 'zod'
import type { EnhancedProductAnalysis, Opportunity, DorkQuery } from '@/lib/types'
import { logServer } from "@/lib/server-logger";
import { appendSerperAgeModifiers, SerperAgeFilter } from "@/lib/serper-date-filters";
// Search result from any source
interface SearchResult {
@@ -31,11 +33,14 @@ const bodySchema = z.object({
problem: z.string(),
searchTerms: z.array(z.string())
}))
})
}),
minAgeDays: z.number().min(0).max(365).optional(),
maxAgeDays: z.number().min(0).max(365).optional(),
})
export async function POST(request: NextRequest) {
try {
const requestId = request.headers.get("x-request-id") ?? undefined;
if (!(await isAuthenticatedNextjs())) {
const redirectUrl = new URL("/auth", request.url);
const referer = request.headers.get("referer");
@@ -45,16 +50,34 @@ export async function POST(request: NextRequest) {
}
const body = await request.json()
const { analysis } = bodySchema.parse(body)
const { analysis, minAgeDays, maxAgeDays } = bodySchema.parse(body)
const ageFilters: SerperAgeFilter = {
minAgeDays,
maxAgeDays,
}
if (!process.env.SERPER_API_KEY) {
await logServer({
level: "warn",
message: "Serper API key missing",
labels: ["api", "search", "config", "warn"],
requestId,
source: "api/search",
});
return NextResponse.json(
{ error: 'SERPER_API_KEY is not configured. Add it to your environment to run searches.' },
{ status: 400 }
)
}
console.log(`🔍 Finding opportunities for: ${analysis.productName}`)
await logServer({
level: "info",
message: "Finding opportunities",
labels: ["api", "search", "start"],
payload: { productName: analysis.productName, filters: ageFilters },
requestId,
source: "api/search",
});
// Sort queries by priority
const sortedQueries = analysis.dorkQueries
@@ -69,22 +92,50 @@ export async function POST(request: NextRequest) {
// Execute searches
for (const query of sortedQueries) {
try {
console.log(` Searching: ${query.query.substring(0, 60)}...`)
const results = await searchGoogle(query.query, 5)
await logServer({
level: "info",
message: "Searching query",
labels: ["api", "search", "query"],
payload: { query: query.query, platform: query.platform },
requestId,
source: "api/search",
});
const results = await searchGoogle(query.query, 5, ageFilters, requestId)
allResults.push(...results)
// Small delay to avoid rate limiting
await new Promise(r => setTimeout(r, 500))
} catch (e) {
console.error(` Search failed for query: ${query.query.substring(0, 40)}`)
await logServer({
level: "error",
message: "Search failed for query",
labels: ["api", "search", "query", "error"],
payload: { query: query.query, error: String(e) },
requestId,
source: "api/search",
});
}
}
console.log(` Found ${allResults.length} raw results`)
await logServer({
level: "info",
message: "Search complete",
labels: ["api", "search", "results"],
payload: { rawResults: allResults.length },
requestId,
source: "api/search",
});
// Analyze and score opportunities
const opportunities = await analyzeOpportunities(allResults, analysis as EnhancedProductAnalysis)
console.log(` ✓ Analyzed ${opportunities.length} opportunities`)
await logServer({
level: "info",
message: "Opportunities analyzed",
labels: ["api", "search", "analyze"],
payload: { analyzed: opportunities.length },
requestId,
source: "api/search",
});
return NextResponse.json({
success: true,
@@ -100,7 +151,18 @@ export async function POST(request: NextRequest) {
})
} catch (error: any) {
console.error('❌ Search error:', error)
await logServer({
level: "error",
message: "Search error",
labels: ["api", "search", "error"],
payload: {
message: error?.message,
stack: error?.stack,
filters: ageFilters,
},
requestId: request.headers.get("x-request-id") ?? undefined,
source: "api/search",
});
return NextResponse.json(
{ error: error.message || 'Failed to find opportunities' },
@@ -109,23 +171,42 @@ export async function POST(request: NextRequest) {
}
}
async function searchGoogle(query: string, num: number): Promise<SearchResult[]> {
return searchSerper(query, num)
async function searchGoogle(
query: string,
num: number,
filters?: SerperAgeFilter,
requestId?: string
): Promise<SearchResult[]> {
return searchSerper(query, num, filters, requestId)
}
async function searchSerper(query: string, num: number): Promise<SearchResult[]> {
async function searchSerper(
query: string,
num: number,
filters?: SerperAgeFilter,
requestId?: string
): Promise<SearchResult[]> {
const filteredQuery = appendSerperAgeModifiers(query, filters)
const response = await fetch('https://google.serper.dev/search', {
method: 'POST',
headers: {
'X-API-KEY': process.env.SERPER_API_KEY!,
'Content-Type': 'application/json'
},
body: JSON.stringify({ q: query, num })
body: JSON.stringify({ q: filteredQuery, num })
})
if (!response.ok) throw new Error('Serper API error')
const data = await response.json()
await logServer({
level: "info",
message: "Serper response received",
labels: ["api", "search", "serper", "response"],
payload: { query: filteredQuery, num, filters, data },
requestId,
source: "api/search",
});
return (data.organic || []).map((r: any) => ({
title: r.title,
url: r.link,