diff --git a/apps/sim/lib/analytics/profound.ts b/apps/sim/lib/analytics/profound.ts deleted file mode 100644 index 80b39ba3526..00000000000 --- a/apps/sim/lib/analytics/profound.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Profound Analytics - Custom log integration - * - * Buffers HTTP request logs in memory and flushes them in batches to Profound's API. - * Runs in Node.js (proxy.ts on ECS), so module-level state persists across requests. - * @see https://docs.tryprofound.com/agent-analytics/custom - */ -import { createLogger } from '@sim/logger' -import { env } from '@/lib/core/config/env' -import { isHosted } from '@/lib/core/config/env-flags' -import { getClientIp } from '@/lib/core/utils/request' -import { getBaseDomain } from '@/lib/core/utils/urls' - -const logger = createLogger('ProfoundAnalytics') - -const FLUSH_INTERVAL_MS = 10_000 -const MAX_BATCH_SIZE = 500 - -interface ProfoundLogEntry { - timestamp: string - method: string - host: string - path: string - status_code: number - ip: string - user_agent: string - query_params?: Record - referer?: string -} - -let buffer: ProfoundLogEntry[] = [] -let flushTimer: NodeJS.Timeout | null = null - -/** - * Returns true if Profound analytics is configured. - */ -export function isProfoundEnabled(): boolean { - return isHosted && Boolean(env.PROFOUND_API_KEY) && Boolean(env.PROFOUND_ENDPOINT) -} - -/** - * Flushes buffered log entries to Profound's API. - */ -async function flush(): Promise { - if (buffer.length === 0) return - - const apiKey = env.PROFOUND_API_KEY - if (!apiKey) { - buffer = [] - return - } - - const endpoint = env.PROFOUND_ENDPOINT - if (!endpoint) { - buffer = [] - return - } - const entries = buffer.splice(0, MAX_BATCH_SIZE) - - try { - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'x-api-key': apiKey, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(entries), - }) - - if (!response.ok) { - logger.error(`Profound API returned ${response.status}`) - } - } catch (error) { - logger.error('Failed to flush logs to Profound', error) - } -} - -function ensureFlushTimer(): void { - if (flushTimer) return - flushTimer = setInterval(() => { - flush().catch(() => {}) - }, FLUSH_INTERVAL_MS) - flushTimer.unref() -} - -/** - * Queues a request log entry for the next batch flush to Profound. - */ -export function sendToProfound(request: Request, statusCode: number): void { - if (!isProfoundEnabled()) return - - try { - const url = new URL(request.url) - const queryParams: Record = {} - url.searchParams.forEach((value, key) => { - queryParams[key] = value - }) - - buffer.push({ - timestamp: new Date().toISOString(), - method: request.method, - host: getBaseDomain(), - path: url.pathname, - status_code: statusCode, - ip: getClientIp(request) ?? '0.0.0.0', - user_agent: request.headers.get('user-agent') || '', - ...(Object.keys(queryParams).length > 0 && { query_params: queryParams }), - ...(request.headers.get('referer') && { referer: request.headers.get('referer')! }), - }) - - ensureFlushTimer() - - if (buffer.length >= MAX_BATCH_SIZE) { - flush().catch(() => {}) - } - } catch (error) { - logger.error('Failed to enqueue log entry', error) - } -} diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 015e3ed5723..7045615176b 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -301,8 +301,6 @@ export const env = createEnv({ TELEMETRY_ENDPOINT: z.string().url().optional(), // Custom telemetry/analytics endpoint COST_MULTIPLIER: z.number().optional(), // Multiplier for cost calculations LOG_LEVEL: z.enum(['DEBUG', 'INFO', 'WARN', 'ERROR']).optional(), // Minimum log level to display (defaults to ERROR in production, DEBUG in development) - PROFOUND_API_KEY: z.string().min(1).optional(), // Profound analytics API key - PROFOUND_ENDPOINT: z.string().url().optional(), // Profound analytics endpoint GRAFANA_OTLP_ENDPOINT: z.string().url().optional(), // Grafana Cloud OTLP HTTP gateway base URL (e.g., https://otlp-gateway-prod-us-east-0.grafana.net/otlp). Trigger.dev exporters append /v1/traces, /v1/logs, /v1/metrics. GRAFANA_OTLP_HEADERS: z.string().min(1).optional(), // Comma-separated key=value headers for OTLP requests (e.g., "Authorization=Basic "). Same format as the OTEL_EXPORTER_OTLP_HEADERS spec. GRAFANA_DEPLOYMENT_ENVIRONMENT: z.string().min(1).optional(), // Deployment tier label (e.g., "production", "staging", "development"). Emitted as the stable `deployment.environment.name` resource attribute on Trigger.dev telemetry to match the rest of the Sim OTEL stack. diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index 402555a43ae..8d4a0ecb2e3 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -1,7 +1,6 @@ import { createLogger } from '@sim/logger' import { getSessionCookie } from 'better-auth/cookies' import { type NextRequest, NextResponse } from 'next/server' -import { sendToProfound } from './lib/analytics/profound' import { getEnv } from './lib/core/config/env' import { isAuthDisabled, isDev, isHosted } from './lib/core/config/env-flags' import { generateRuntimeCSP } from './lib/core/security/csp' @@ -316,40 +315,40 @@ export async function proxy(request: NextRequest) { const hasActiveSession = isAuthDisabled || !!sessionCookie const redirect = handleRootPathRedirects(request, hasActiveSession) - if (redirect) return track(request, redirect) + if (redirect) return applyIndexingPolicy(request, redirect) if (url.pathname === '/login' || url.pathname === '/signup') { if (hasActiveSession) { - return track(request, NextResponse.redirect(new URL('/workspace', request.url))) + return applyIndexingPolicy(request, NextResponse.redirect(new URL('/workspace', request.url))) } const response = NextResponse.next() response.headers.set('Content-Security-Policy', generateRuntimeCSP()) response.headers.set('X-Content-Type-Options', 'nosniff') response.headers.set('X-Frame-Options', 'SAMEORIGIN') - return track(request, response) + return applyIndexingPolicy(request, response) } // Chat pages are publicly accessible embeds — CSP is set in next.config.ts headers if (url.pathname.startsWith('/chat/')) { - return track(request, NextResponse.next()) + return applyIndexingPolicy(request, NextResponse.next()) } if (url.pathname.startsWith('/workspace')) { if (!hasActiveSession) { - return track(request, NextResponse.redirect(new URL('/login', request.url))) + return applyIndexingPolicy(request, NextResponse.redirect(new URL('/login', request.url))) } const response = NextResponse.next() response.headers.set('Content-Security-Policy', generateRuntimeCSP()) response.headers.set('X-Content-Type-Options', 'nosniff') response.headers.set('X-Frame-Options', 'SAMEORIGIN') - return track(request, response) + return applyIndexingPolicy(request, response) } const invitationRedirect = handleInvitationRedirects(request, hasActiveSession) - if (invitationRedirect) return track(request, invitationRedirect) + if (invitationRedirect) return applyIndexingPolicy(request, invitationRedirect) const securityBlock = handleSecurityFiltering(request) - if (securityBlock) return track(request, securityBlock) + if (securityBlock) return applyIndexingPolicy(request, securityBlock) const response = NextResponse.next() response.headers.set('Vary', 'User-Agent') @@ -358,7 +357,7 @@ export async function proxy(request: NextRequest) { response.headers.set('X-Content-Type-Options', 'nosniff') response.headers.set('X-Frame-Options', 'SAMEORIGIN') - return track(request, response) + return applyIndexingPolicy(request, response) } /** @@ -370,7 +369,7 @@ export async function proxy(request: NextRequest) { * the index. robots.txt is excluded from this proxy's matcher so it keeps * serving the crawlable rules this header depends on. */ -function applyIndexingPolicy(request: NextRequest, response: NextResponse): void { +function applyIndexingPolicy(request: NextRequest, response: NextResponse): NextResponse { const host = request.headers.get('x-forwarded-host')?.split(',')[0]?.trim() || request.headers.get('host') || @@ -379,14 +378,7 @@ function applyIndexingPolicy(request: NextRequest, response: NextResponse): void if (isNonCanonicalSimHost(host)) { response.headers.set('X-Robots-Tag', 'noindex, nofollow') } -} -/** - * Sends request data to Profound analytics (fire-and-forget) and returns the response. - */ -function track(request: NextRequest, response: NextResponse): NextResponse { - applyIndexingPolicy(request, response) - sendToProfound(request, response.status) return response }