Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a10da3e
fix(BUY-5978): add APP_BASE_URL to buywhere-api Cloud Run and add hea…
May 1, 2026
392b31e
fix(BUY-5978): add trackProductSearch/trackProductView exports to pos…
May 1, 2026
9712dbb
fix(BUY-5978): point MCP Cloud Run probes at /healthz (no DB dependen…
May 1, 2026
cecfff7
fix(MCP): forward raw Bearer token to internal API calls
May 1, 2026
d69319f
BUY-3908: Recreate inject-posthog-vm.yml for production VM POSTHOG_AP…
May 1, 2026
13ee316
BUY-6436: fix MCP integration guide — replace stale buywhere-mcp with…
May 2, 2026
eb804f0
feat: add public APIs.json descriptor for API index submission
May 2, 2026
1aba47a
BUY-6478: fix stale MCP docs — replace buywhere-mcp with @buywhere/mc…
May 2, 2026
e0e7ab4
feat(BUY-6594): add explicit /mcp/ location block to api.buywhere.ai.…
May 2, 2026
0ca734f
BUY-2814 Phase 2: add Amazon AU, Zalora MY scrapers + ECS task defini…
May 2, 2026
9590297
feat: add best-headphones-singapore SEO landing page
May 2, 2026
7f02cd9
feat: update developer portal and quickstart metadata for SEO
May 2, 2026
49ad451
fix: correct PAPERCLIP_API_URL default to api.paperclip.ai
May 2, 2026
9e4dfc8
feat: update integrate page metadata for SEO discoverability
May 2, 2026
42561a2
feat: update pricing page metadata for SEO discoverability
May 2, 2026
9ea1a98
fix: route /mcp directly to port 8000 (Node.js API) instead of api_ba…
May 2, 2026
7fa5679
feat: update electronics category page SEO for discoverability
May 2, 2026
0e0c8f3
feat: update fashion category page SEO for discoverability
May 2, 2026
0aeed85
fix: improve beauty-health + grocery category page SEO metadata and s…
May 2, 2026
e53ad5a
fix: improve home-living category page SEO metadata and schema
May 2, 2026
60aad37
fix(BUY-7240): add healthz route to Next.js and nginx config for Clou…
May 2, 2026
c8f7c62
fix(nginx-deploy): use sudo for nginx config writes
May 2, 2026
713fd30
fix(scraper): align shopee_sg.py with /v1/products/ingest API contract
May 2, 2026
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
101 changes: 101 additions & 0 deletions .github/workflows/inject-posthog-vm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Inject PostHog Key to Production VM

on:
workflow_dispatch:

permissions:
contents: read

jobs:
inject-posthog-vm:
name: Inject POSTHOG_API_KEY to Production VM
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up SSH agent
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.PRODUCTION_DEPLOY_SSH_KEY }}

- name: Trust production host
run: |
mkdir -p ~/.ssh
ssh-keyscan -p "${{ secrets.PRODUCTION_DEPLOY_PORT || 22 }}" -H "${{ secrets.PRODUCTION_DEPLOY_HOST }}" >> ~/.ssh/known_hosts

- name: Detect service and inject POSTHOG_API_KEY
env:
DEPLOY_HOST: ${{ secrets.PRODUCTION_DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.PRODUCTION_DEPLOY_PORT || 22 }}
DEPLOY_USER: ${{ secrets.PRODUCTION_DEPLOY_USER }}
POSTHOG_KEY: ${{ secrets.POSTHOG_API_KEY_PRODUCTION }}
run: |
ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" << 'EOF'
set -euo pipefail

echo "=== Detecting service management ==="

POSTHOG_KEY="${POSTHOG_KEY}"

# Check for systemd service
if systemctl list-unit-files 2>/dev/null | grep -qiE 'buywhere|fastapi|uvicorn'; then
echo "Detected: systemd-managed"
SVC=$(systemctl list-units --type=service --all 2>/dev/null | grep -iE 'buywhere|fastapi|uvicorn' | awk '{print $1}' | head -1)
SVC="${SVC:-buywhere-api}"

# Update env file
ENV_FILE="/etc/systemd/system/${SVC}.d/override.conf"
mkdir -p "$(dirname $ENV_FILE)"
if ! grep -q "POSTHOG_API_KEY=" "$ENV_FILE" 2>/dev/null; then
echo "POSTHOG_API_KEY=$POSTHOG_KEY" >> "$ENV_FILE"
fi
systemctl daemon-reload
systemctl restart "$SVC" || true
echo "Done: systemd updated and service restarted"

# Check for PM2
elif command -v pm2 &>/dev/null && pm2 list 2>/dev/null | grep -qiE 'buywhere|api|fastapi'; then
echo "Detected: PM2-managed"
PM2_NAME=$(pm2 list 2>/dev/null | grep -iE 'buywhere|api|fastapi' | awk '{print $2}' | head -1)
if [ -n "$PM2_NAME" ]; then
export POSTHOG_API_KEY="$POSTHOG_KEY"
pm2 restart "$PM2_NAME" || true
echo "Done: PM2 process restarted"
fi

# Check for Docker
elif command -v docker &>/dev/null && docker ps 2>/dev/null | grep -qiE 'buywhere|api'; then
echo "Detected: Docker-managed"
CONTAINER=$(docker ps 2>/dev/null | grep -iE 'buywhere|api' | awk '{print $1}' | head -1)
if [ -n "$CONTAINER" ]; then
docker exec "$CONTAINER" env POSTHOG_API_KEY="$POSTHOG_KEY" sh -c 'echo "POSTHOG_API_KEY set"' 2>/dev/null || true
docker restart "$CONTAINER" 2>/dev/null || true
echo "Done: Docker container restarted"
fi

# Check for raw process
elif pgrep -f "uvicorn\|gunicorn" &>/dev/null; then
echo "Detected: Raw process (uvicorn/gunicorn)"
# Add to /etc/environment as fallback
if ! grep -q "POSTHOG_API_KEY=" /etc/environment 2>/dev/null; then
echo "POSTHOG_API_KEY=$POSTHOG_KEY" >> /etc/environment
fi
export POSTHOG_API_KEY="$POSTHOG_KEY"
PID=$(pgrep -f "uvicorn\|gunicorn" | head -1)
echo "Process PID: $PID — killed for restart"
kill -TERM "$PID" 2>/dev/null || true
sleep 2
# Restart hint
echo "WARNING: Manual restart required for raw process"
else
echo "No known service found — adding to /etc/environment"
if ! grep -q "POSTHOG_API_KEY=" /etc/environment 2>/dev/null; then
echo "POSTHOG_API_KEY=$POSTHOG_KEY" >> /etc/environment
fi
echo "Added to /etc/environment — verify service picks it up"
fi

echo "=== PostHog key injection complete ==="
EOF
6 changes: 3 additions & 3 deletions .github/workflows/nginx-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ jobs:

# Validate before touching live config
nginx -t -c /etc/nginx/nginx.conf 2>&1 || true
cp "${SRC}" "${DEST}"
nginx -t
sudo cp "${SRC}" "${DEST}"
sudo nginx -t

if [[ "${DRY_RUN}" == "true" ]]; then
echo "DRY RUN: config validated OK, skipping reload"
exit 0
fi

nginx -s reload
sudo nginx -s reload
echo "nginx reloaded — ${CONFIG_NAME} is live (sha ${DEPLOY_SHA})"

# Cleanup tmp
Expand Down
32 changes: 32 additions & 0 deletions api/dist/analytics/posthog.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.trackApiQuery = trackApiQuery;
exports.trackAffiliateClick = trackAffiliateClick;
exports.trackRegistration = trackRegistration;
exports.trackProductSearch = trackProductSearch;
exports.trackProductView = trackProductView;
exports.trackComparePageView = trackComparePageView;
exports.trackCompareRetailerClick = trackCompareRetailerClick;
exports.shutdownPostHog = shutdownPostHog;
Expand Down Expand Up @@ -77,6 +79,36 @@ function trackRegistration(apiKey, agentName, signupChannel, utmSource) {
},
});
}
function trackProductSearch(event) {
const ph = getClient();
if (!ph)
return;
ph.capture({
distinctId: event.apiKey,
event: 'product_search',
properties: {
query_text: event.queryText,
result_count: event.resultCount,
response_time_ms: event.responseTimeMs,
source_page: event.sourcePage,
},
});
}
function trackProductView(event) {
const ph = getClient();
if (!ph)
return;
ph.capture({
distinctId: event.apiKey || 'anonymous',
event: 'product_view',
properties: {
product_id: event.productId,
retailer: event.retailer,
category: event.category,
source: event.source,
},
});
}
function trackComparePageView(event) {
const ph = getClient();
if (!ph)
Expand Down
3 changes: 3 additions & 0 deletions api/dist/mcp-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ app.get('/health', async (_req, res) => {
res.status(500).json({ status: 'error', error: String(err) });
}
});
app.get('/healthz', (_req, res) => {
res.json({ status: 'ok', server: 'mcp' });
});
app.use('/mcp', mcp_1.default);
// JSON-RPC root alias — allow POST / as shorthand for POST /mcp
app.use('/', mcp_1.default);
Expand Down
18 changes: 15 additions & 3 deletions api/dist/routes/products.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const router = (0, express_1.Router)();
// GET /v1/products/search
// Query params: q, domain, region, country, min_price, max_price, currency, limit, offset, source_page
router.get('/search', agentDetect_1.agentDetectMiddleware, apiKey_1.requireApiKey, apiKey_1.checkRateLimit, (0, queryLog_1.queryLogMiddleware)('products.search'), async (req, res) => {
const start = Date.now();
const requestStart = Date.now();
const q = req.query.q || '';
const domain = req.query.domain;
const region = req.query.region;
Expand All @@ -37,7 +37,7 @@ router.get('/search', agentDetect_1.agentDetectMiddleware, apiKey_1.requireApiKe
const cached = await config_1.redis.get(cacheKey);
if (cached) {
const parsed = JSON.parse(cached);
const elapsed = Date.now() - start;
const elapsed = Date.now() - requestStart;
// compact envelope uses flat keys; legacy uses nested meta
if (parsed.meta) {
parsed.meta.cached = true;
Expand Down Expand Up @@ -152,7 +152,7 @@ router.get('/search', agentDetect_1.agentDetectMiddleware, apiKey_1.requireApiKe
params.push(limit, offset);
const dataResult = await config_1.db.query(dataQuery, params);
const total = parseInt(countResult.rows[0].count, 10);
const responseTimeMs = Date.now() - start;
const responseTimeMs = Date.now() - requestStart;
const products = dataResult.rows.map((row) => {
if (compact) {
// Compact format for AI agents (BUY-2073): Phase 2 shape.
Expand Down Expand Up @@ -250,6 +250,12 @@ router.get('/search', agentDetect_1.agentDetectMiddleware, apiKey_1.requireApiKe
sourcePage: sourcePage || null,
endpoint: 'products.search',
});
(0, posthog_1.trackProductSearch)({
apiKey: (0, apiKey_1.hashKey)(req.apiKeyRecord.key),
queryText: q,
resultCount: products.length,
responseTimeMs,
});
}
res.json(responseBody);
});
Expand Down Expand Up @@ -553,6 +559,12 @@ router.get('/:id', agentDetect_1.agentDetectMiddleware, apiKey_1.requireApiKey,
sourcePage: null,
endpoint: 'products.get',
});
(0, posthog_1.trackProductView)({
apiKey: (0, apiKey_1.hashKey)(req.apiKeyRecord.key),
productId: row.id,
retailer: row.domain,
category: (row.category_path ? row.category_path.split(' > ')[0] : null),
});
}
res.json({ data: product });
});
Expand Down
46 changes: 46 additions & 0 deletions api/src/analytics/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,52 @@ export function trackRegistration(apiKey: string, agentName: string, signupChann
});
}

export interface ProductSearchEvent {
apiKey: string;
queryText: string;
resultCount: number;
responseTimeMs: number;
sourcePage?: string | null;
}

export function trackProductSearch(event: ProductSearchEvent): void {
const ph = getClient();
if (!ph) return;
ph.capture({
distinctId: event.apiKey,
event: 'product_search',
properties: {
query_text: event.queryText,
result_count: event.resultCount,
response_time_ms: event.responseTimeMs,
source_page: event.sourcePage,
},
});
}

export interface ProductViewEvent {
apiKey: string | null;
productId: string;
retailer: string;
category: string | null;
source?: string | null;
}

export function trackProductView(event: ProductViewEvent): void {
const ph = getClient();
if (!ph) return;
ph.capture({
distinctId: event.apiKey || 'anonymous',
event: 'product_view',
properties: {
product_id: event.productId,
retailer: event.retailer,
category: event.category,
source: event.source,
},
});
}

export interface ComparePageViewEvent {
slug: string;
productId: string;
Expand Down
Loading
Loading