Skip to content

Comments

Add CostLens Project#57

Open
MANASMATHUR wants to merge 3 commits intotinyfish-io:mainfrom
MANASMATHUR:manas/costlens
Open

Add CostLens Project#57
MANASMATHUR wants to merge 3 commits intotinyfish-io:mainfrom
MANASMATHUR:manas/costlens

Conversation

@MANASMATHUR
Copy link
Contributor

@MANASMATHUR MANASMATHUR commented Feb 16, 2026

CostLens is an open-source intelligence tool that reverse-engineers the true cost of any SaaS product by analyzing it across five pillars: Infrastructure, Build, Buyer, Risk, and Competitors.

Deployed Link :-naked-saa-s-tiny-fish.vercel.app

Summary by CodeRabbit

New Features

  • Cost Analysis Dashboard: Five-pillar analysis system covering Infrastructure, Build, Buyer, Risk, and Competitor assessment
  • Real-Time Scanning: Live progress tracking with step-by-step investigation updates
  • Report Generation: Comprehensive reports with executive summaries, recommendations, and detailed breakdowns
  • Report Management: Scan history with load/delete functionality and data persistence
  • Data Export: Download reports as JSON or copy formatted text summaries
  • Negotiation & Competitive Intelligence: Playbook guidance and competitive positioning analysis

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces CostLens, a complete full-stack SaaS cost analysis platform featuring a React frontend dashboard, Express backend server with multi-pillar cost analysis endpoints (infrastructure, build, buyer, risk, competitor), TinyFish web agent integration for data collection, OpenAI-powered AI cost modeling, and configuration for local development and Vercel deployment.

Changes

Cohort / File(s) Summary
Project Configuration
costlens/.env.example, costlens/.gitignore, costlens/README.md, costlens/package.json, costlens/vite.config.js, costlens/vercel.json
New project setup including environment variables, dependencies, build/dev scripts, Vite React config, and Vercel deployment settings with per-endpoint duration limits.
Client Entry Point
costlens/client/index.html, costlens/client/src/main.jsx, costlens/client/src/App.jsx
Core React application initialization with root component managing global state (results, views, scanning), investigation workflow orchestration, poll retry logic, and routing to views based on analysis state.
Client UI Components
costlens/client/src/components/AppHeader.jsx, costlens/client/src/components/Badges.jsx, costlens/client/src/components/ExecutiveSummary.jsx, costlens/client/src/components/ExportBar.jsx, costlens/client/src/components/InfraVisualizations.jsx, costlens/client/src/components/NegotiationPlaybook.jsx, costlens/client/src/components/ReportSummary.jsx, costlens/client/src/components/ScanHistory.jsx, costlens/client/src/components/ScanOverlay.jsx
Specialized report display and navigation components including header tabs, confidence/complexity badges, executive summary rendering, data visualizations (gauge, stacked bar), negotiation playbook, scan history with localStorage integration, and progress overlay.
Client Primitive Components
costlens/client/src/components/ui/Panel.jsx, costlens/client/src/components/ui/SectionLabel.jsx, costlens/client/src/components/ui/DegradedBanner.jsx, costlens/client/src/components/ui/ExpandableRow.jsx, costlens/client/src/components/ui/WhyHint.jsx
Reusable UI building blocks for layout (Panel), typography (SectionLabel), status messaging (DegradedBanner), collapsible content (ExpandableRow), and contextual hints (WhyHint).
Client Design System
costlens/client/src/styles/tokens.js
Centralized design tokens defining colors, typography scale, spacing scale, and border radius values.
Client Utility Modules
costlens/client/src/utils/formatting.js, costlens/client/src/utils/export.js, costlens/client/src/utils/history.js, costlens/client/src/utils/report.js
Utilities for numeric/text formatting, JSON/text export with file download, localStorage-based scan history, and deep report normalization with type coercion and fallback handling.
Client Analysis Views
costlens/client/src/views/LandingView.jsx, costlens/client/src/views/InfraView.jsx, costlens/client/src/views/BuildView.jsx, costlens/client/src/views/BuyerView.jsx, costlens/client/src/views/RiskView.jsx, costlens/client/src/views/CompetitorView.jsx
Pillar-specific dashboard views rendering cost estimates, confidence metrics, detailed breakdowns, provenance sources, degradation warnings, and interactive expandable sections for infrastructure, build, buyer, risk, and competitive analysis.
Server API Route Handlers
costlens/api/health.js, costlens/api/investigate.js, costlens/api/investigate/async.js, costlens/api/investigate/async/poll.js, costlens/api/investigate/stream.js
Vercel serverless function re-exports delegating to Express server app for health checks, sync/async investigation, polling, and streaming endpoints.
Server Core
costlens/server/server.js, costlens/server/config/index.js
Express application with CORS/rate limiting, endpoints for health, async investigation, polling with AI synthesis, streaming SSE, URL validation, pillar metadata computation, quality scoring, and environment readiness checks.
Server AI Analysis
costlens/server/analysis/cost-modeler.js
Comprehensive CostModeler orchestrating OpenAI-based cost analysis across infra/build/buyer pillars with normalization, validation, anomaly detection, executive summary generation, negotiation playbook synthesis, risk profile analysis, and competitor landscape mapping.
Server Data Collection Services
costlens/server/services/infra-cost-scanner.js, costlens/server/services/build-cost-estimator.js, costlens/server/services/buyer-cost-analyzer.js, costlens/server/services/tech-risk-scanner.js
Modular services leveraging TinyFish client to gather infrastructure signals, build/hiring costs, pricing/review data, and security/privacy/tracker risk signals with fast/slow mode support and metadata tracking.
Server Integration
costlens/server/tinyfish/tinyfish-web-agent-client.js
TinyFish web agent API client with SSE support, retry logic, timeout handling, proxy configuration, JSON/automation request modes, and consistent error parsing.
Testing
costlens/server/test/api.test.js, costlens/server/scripts/smoke-report-shape.js
API endpoint tests validating health, investigate, and payload structure; smoke tests verifying CostModeler output shapes and data types.

Sequence Diagrams

sequenceDiagram
    participant Client as Client<br/>(React)
    participant Server as Server<br/>(Express)
    participant TinyFish as TinyFish<br/>Web Agent
    participant OpenAI as OpenAI<br/>API
    participant LocalStorage as LocalStorage

    Client->>Server: POST /api/investigate/async<br/>(url)
    activate Server
    Server->>TinyFish: runAsync (infra, build, buyer, risk, competitors)
    activate TinyFish
    TinyFish-->>Server: runIds[] (5 parallel runs)
    deactivate TinyFish
    Server-->>Client: { runIds, status }
    deactivate Server

    loop Poll Results
        Client->>Server: POST /api/investigate/async/poll<br/>(runIds)
        activate Server
        Server->>TinyFish: getRun(runId) [×5]
        activate TinyFish
        TinyFish-->>Server: results[] (when complete)
        deactivate TinyFish
        
        Server->>OpenAI: synthesize costs<br/>(infra, build, buyer)
        activate OpenAI
        OpenAI-->>Server: costModels + executive
        deactivate OpenAI
        
        Server-->>Client: { report, quality }
        deactivate Server
    end

    Client->>LocalStorage: saveReport()
    Client->>Client: display views<br/>(infra, build, buyer, risk)
Loading
sequenceDiagram
    participant Client as Client<br/>(React)
    participant Server as Server<br/>(Express)
    participant TinyFish as TinyFish<br/>Web Agent
    participant OpenAI as OpenAI<br/>API

    Client->>Server: GET /api/investigate/stream<br/>(url)
    activate Server
    Server->>Client: SSE stream open
    
    Server->>TinyFish: runAutomation (5 pillars)
    activate TinyFish
    
    loop For Each Pillar
        TinyFish-->>Server: SSE event
        Server-->>Client: progress event
    end
    
    TinyFish-->>Server: complete
    deactivate TinyFish
    
    Server->>OpenAI: synthesize report
    activate OpenAI
    OpenAI-->>Server: complete report
    deactivate OpenAI
    
    Server-->>Client: final event + close
    deactivate Server
    
    Client->>Client: render results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The PR introduces a substantial, heterogeneous full-stack application spanning 50+ new files with diverse logic density. While individual components are modular, the complexity stems from: intricate investigation workflows in server.js and cost-modeler.js (~800 and ~970 lines respectively), multi-stage data normalization in report.js (~500 lines), diverse service implementations, client-side state management across views, and integration with external APIs (TinyFish, OpenAI). The code exhibits consistent patterns and good separation of concerns, but comprehensive review requires understanding multiple interdependent systems and verifying logic across infra/build/buyer/risk/competitor analysis pipelines.

Poem

🐰 A rabbit hops with delight, for CostLens takes flight!
Five pillars of insight, from infra to buyer so bright,
TinyFish and OpenAI dance through the night,
With dashboards and streams and reports shining light,
The cost of it all, now crystal and right! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add CostLens Project' accurately describes the main change: introducing a complete new project directory with full frontend, backend, and infrastructure components.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@MANASMATHUR
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@costlens/client/src/components/ExecutiveSummary.jsx`:
- Line 21: The destructured props from executiveSummary (summary, keyFindings,
recommendations, verdictLabel) can be undefined so accessing keyFindings.length
and recommendations.length may throw; fix by defaulting them to safe values when
destructuring (e.g., destructure from executiveSummary || {} and set keyFindings
= [] and recommendations = []) and/or add guards before using .length so the
component (ExecutiveSummary.jsx) always operates on arrays (use keyFindings = []
and recommendations = [] in the const { ... } assignment or equivalent
null-coalescing checks).

In `@costlens/client/src/components/NegotiationPlaybook.jsx`:
- Around line 8-10: Destructuring from negotiation can yield undefined
sub-properties and cause a TypeError when calling .length; update the
destructuring of negotiation (leverageFactors, talkingPoints, counterOffers,
riskWarnings) to default each to [] and include riskWarnings in the hasContent
check so the component safely handles missing backend fields and remains
consistent with its conditional rendering (look for the negotiation
destructuring and the hasContent variable).

In `@costlens/client/src/components/ReportSummary.jsx`:
- Around line 23-38: The map callback for checklistPillars uses a parameter
named pillar which shadows the outer pillar variable and is error-prone; update
the callback parameter name in checklistPillars.map (and the other .map at the
same file) to a non-conflicting identifier such as pillarKey or p, then update
all uses inside that callback (e.g., the pillar -> pillarKey references used to
build the returned object: label, status, details) so the outer pillar variable
is no longer shadowed.

In `@costlens/client/src/components/ui/ExpandableRow.jsx`:
- Around line 14-58: The ExpandableRow component currently renders subtitle and
expanded children inside a single <button>, which creates invalid nested
interactive content; refactor ExpandableRow so the <button> only contains the
clickable header (title, badge, titleRight and the expand icon) and move
subtitle and the expanded content (children) outside the button into a sibling
container (e.g., a <div> under the button); keep using the expanded prop and
onToggle handler on the header button, add an id on the content container and
set aria-controls on the button (and preserve aria-expanded and
accessibilityLabel), and ensure the expanded content is conditionally rendered
outside the button so nested interactive elements in children (like WhyHint) are
not placed inside a button.

In `@costlens/client/src/views/LandingView.jsx`:
- Line 43: The Enter key handler in LandingView.jsx currently calls runScan()
unconditionally, bypassing the button's disabled guard; update the onKeyDown
handler to only trigger runScan() when the same conditions as the button are met
(e.g., !scanning && url.trim()), and optionally call e.preventDefault() to avoid
duplicate submits. Locate the onKeyDown usage in LandingView.jsx and change its
logic so it checks scanning and url.trim() before invoking runScan() (and keep
using the existing runScan() function and scanning/url state variables).

In `@costlens/server/server.js`:
- Around line 363-453: Duplicate report-assembly logic (from failedPillars
calculation through the returned report object) exists in runInvestigation and
the poll endpoint; extract it into a shared assembleReport(...) function and
call it from both places. Create assembleReport({ report, executiveSummary,
negotiation, riskProfile, competitorAnalysis, scannerErrors, infraRaw, buildRaw,
buyerRaw, riskRaw, timedOut, config, name, domain }) that performs the
failedPillars/degradedPillars computation, builds pillarMeta via
normalizePillarMeta, computes qualityMeta via buildQualityMeta, calculates
legacyCompleteness, and returns the final report object exactly as in the diff
(including timeout merging into scannerErrors, provenance fields, and default
riskProfile using modeler.getDefaultRiskProfile() when needed); then replace the
duplicated blocks in runInvestigation and the poll endpoint with calls to
assembleReport, ensuring competitorAnalysis is optional so the sync path
behavior remains unchanged.
- Around line 663-824: The poll handler at
app.post("/api/investigate/async/poll") is re-running full AI synthesis
(CostModeler.analyze and subsequent modeler.generate* calls) on every completed
poll, causing duplicate expensive API calls; fix by introducing a short-lived
synthesis cache or status keyed by the runIds before calling CostModeler.
Specifically, before invoking CostModeler.analyze or
modeler.generateExecutiveSummary/generateNegotiationPlaybook/analyzeRiskProfile/generateCompetitorAnalysis,
check a shared store (e.g., an in-memory Map or short-lived KV) using a
deterministic key derived from runIds; if an entry exists return the cached
synthesized report (or a saved status like "synthesizing") instead of
re-running; when starting synthesis set the key to a "synthesizing" marker so
concurrent polls return status:"synthesizing", and once synthesis completes
store the final report under that key with an expiration TTL to avoid stale
data. Ensure you reference the runIds object and store the final response body
structure you currently send so subsequent polls return identical JSON.
- Around line 60-65: The current Vercel check (isVercelDeployment +
/^https:\/\/[a-z0-9-]+\.vercel\.app$/i.test(origin)) is too permissive; narrow
it to only your project's Vercel host or make it configurable. Replace the
blanket regex with a project-specific pattern (for example require the subdomain
to start with "costlens-" such as
/^https:\/\/costlens-[a-z0-9-]+\.vercel\.app$/i.test(origin)) or read an allowed
Vercel hostname/pattern from an env var (e.g., ALLOWED_VERCEL_HOST or
VERCEL_PROJECT_PREFIX) and validate origin against that value, keeping the
existing isVercelDeployment and callback logic in the same place.

In `@costlens/server/tinyfish/tinyfish-web-agent-client.js`:
- Around line 95-153: The SSE stream in _runSse can hang because sseTimeoutMs
only applies to the initial _fetchWithTimeout; fix by wiring an AbortController
into the streaming phase: create an AbortController before calling
_fetchWithTimeout (or modify _fetchWithTimeout to accept a signal) and pass its
signal to fetch, then start a timeout timer for sseTimeoutMs that calls
controller.abort() if no progress; inside the reader loop reset or refresh that
timer each time you successfully decode a chunk (so active streams don’t time
out) and clear the timer/close the reader on completion or error; ensure abort
errors are caught and turned into a clear timeout Error rather than leaving
reader.read() hung.
🟡 Minor comments (20)
costlens/client/src/components/InfraVisualizations.jsx-33-38 (1)

33-38: ⚠️ Potential issue | 🟡 Minor

items is accessed without a null guard.

If items is undefined or null, the .filter() call on Line 38 (and Line 60) will throw. Consider a default parameter:

-export function CostBar({ items }) {
+export function CostBar({ items = [] }) {
costlens/client/src/components/InfraVisualizations.jsx-21-27 (1)

21-27: ⚠️ Potential issue | 🟡 Minor

No defensive handling if low, mid, or high are undefined/NaN.

mid.toFixed(0) (Line 21), low.toFixed(1) and high.toFixed(1) (Line 27) will throw a TypeError if the parent passes undefined or null. Consider defaulting props or adding a guard, e.g.:

-export function MarginGauge({ low, mid, high }) {
+export function MarginGauge({ low = 0, mid = 0, high = 0 }) {
costlens/package.json-17-25 (1)

17-25: ⚠️ Potential issue | 🟡 Minor

Move react and react-dom to devDependencies.

This project uses a client/server architecture: the client is a Vite-built React SPA, and the server is a pure Express API that serves static assets and endpoints. React is only needed at build time to compile the client; the Express server does not require it at runtime.

♻️ Move to devDependencies
   "dependencies": {
     "cors": "^2.8.5",
     "dotenv": "^17.3.1",
     "express": "^4.18.2",
     "express-rate-limit": "^8.2.1",
-    "openai": "^4.28.0",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "openai": "^4.28.0"
   },
   "devDependencies": {
     "@vitejs/plugin-react": "^4.2.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
     "supertest": "^7.0.0",
     "vite": "^5.0.0"
   }
costlens/server/services/infra-cost-scanner.js-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor

Misleading comment: "Fast: 2 runs" mode doesn't exist in the code.

The comment says "Ultra-fast: 1 combined run for infra (tech + traffic). Fast: 2 runs. Full: 4 runs." but the code only implements two modes: fast (1 run) and full (4 runs). There is no 2-run mode.

costlens/server/services/infra-cost-scanner.js-50-71 (1)

50-71: ⚠️ Potential issue | 🟡 Minor

raw.techStack ?? raw may produce confusing results when the AI doesn't nest under techStack.

On Line 68, if response.result is a flat object without a techStack key, the entire result gets assigned as techStack. This could make downstream consumers receive traffic data mixed into the techStack field. Consider falling back to null or {} instead.

-      techStack: raw.techStack ?? raw,
+      techStack: raw.techStack ?? null,
costlens/client/src/utils/export.js-44-48 (1)

44-48: ⚠️ Potential issue | 🟡 Minor

Biome lint: forEach callback should not return a value.

The arrow shorthand (f) => lines.push(...) implicitly returns the result of push. Use a block body to silence the lint rule. Same pattern appears on lines 79, 100.

Proposed fix (apply similarly to lines 79 and 100)
-      report.executiveSummary.keyFindings.forEach((f) => lines.push(`  • ${f}`));
+      report.executiveSummary.keyFindings.forEach((f) => { lines.push(`  • ${f}`); });
costlens/server/config/index.js-4-5 (1)

4-5: ⚠️ Potential issue | 🟡 Minor

CORS default may block the Vite dev client.

corsOrigin defaults to http://localhost:3000, which is also the server's default port. Vite typically serves on port 5173, so local development requests from the client will be blocked by CORS unless CORS_ORIGIN is explicitly set.

Suggested fix
- corsOrigin: process.env.CORS_ORIGIN || "http://localhost:3000",
+ corsOrigin: process.env.CORS_ORIGIN || "http://localhost:5173",
costlens/client/src/components/AppHeader.jsx-106-108 (1)

106-108: ⚠️ Potential issue | 🟡 Minor

degradedSet and degradedReason not guarded against undefined.

If the parent omits these props, degradedSet.has(id) throws. Consider defaulting in the destructuring.

Proposed fix
-export function AppHeader({ hasResults, view, onViewChange, onGoHome, tabMeta, degradedSet, degradedReason }) {
+export function AppHeader({ hasResults, view, onViewChange, onGoHome, tabMeta, degradedSet = new Set(), degradedReason = () => "" }) {
costlens/client/src/views/RiskView.jsx-59-59 (1)

59-59: ⚠️ Potential issue | 🟡 Minor

ScoreGauge may render a misleading "0" when no security score exists.

hasMeaningfulData on line 59 is true when findings.length > 0 || badges.length > 0, even if securityScore is 0. In that case ScoreGauge renders a gauge showing "0", which looks like a measured zero rather than "no data." Consider gating the gauge on risk.securityScore > 0 separately.

🔧 Proposed fix
         <Panel style={{ padding: 24, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}>
-          {hasMeaningfulData ? (
+          {risk.securityScore > 0 ? (
             <ScoreGauge score={risk.securityScore} label="Security Score" />
           ) : (
             <div style={{ fontFamily: "'IBM Plex Mono',monospace", fontSize: type.sizeSm, color: colors.textMuted, textAlign: "center" }}>
               Insufficient data for security score
             </div>
           )}
         </Panel>

Also applies to: 82-90

costlens/client/src/components/ScanHistory.jsx-91-107 (1)

91-107: ⚠️ Potential issue | 🟡 Minor

Missing Space key handler on the delete control.

role="button" elements should activate on both Enter and Space per ARIA authoring practices. Currently only Enter is handled, so keyboard users pressing Space will scroll the page instead of deleting.

🔧 Proposed fix
-                onKeyDown={(e) => { if (e.key === "Enter") handleDelete(entry.id, e); }}
+                onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleDelete(entry.id, e); } }}
costlens/server/scripts/smoke-report-shape.js-16-24 (1)

16-24: ⚠️ Potential issue | 🟡 Minor

Unsafe buyer.plans[0] access — crashes if default plans array is empty.

Line 21 accesses buyer.plans[0].name without a length guard. If getDefaultBuyerCost() ever returns an empty plans array (or the normalization changes), this will throw a TypeError at runtime. The assertion on Line 18 only checks Array.isArray, not length.

🛡️ Proposed fix: add a length assertion before index access
  assert(Array.isArray(buyer.plans), "Buyer plans should always be an array");
+ assert(buyer.plans.length > 0, "Buyer default plans should contain at least one entry");
  assert(typeof infra.monthlyEstimate.low === "number", "Infra monthly low should be numeric");
  assert(typeof build.teamSize.min === "number", "Build team min should be numeric");
  assert(typeof buyer.plans[0].name === "string", "Buyer plan name should be string");
costlens/server/test/api.test.js-33-38 (1)

33-38: ⚠️ Potential issue | 🟡 Minor

Accepting HTTP 500 weakens the test.

Allowing 500 alongside 200 and 400 on line 35 means an internal server crash is silently accepted. If the intent is to handle missing env vars gracefully, the server should return 400 with a missingEnv payload — not 500. Consider tightening this to [200, 400] and treating 500 as a test failure.

🔧 Proposed fix
-  assert.ok([200, 400, 500].includes(validUrlRes.status), "POST with valid URL should return 200, 400, or 500 depending on env and providers");
+  assert.ok([200, 400].includes(validUrlRes.status), "POST with valid URL should return 200 or 400 (500 indicates an unhandled error)");
costlens/client/src/utils/report.js-351-351 (1)

351-351: ⚠️ Potential issue | 🟡 Minor

completenessScore defaults to 100 when missing — should likely be 0.

If the server omits completenessScore, the report silently appears fully complete. A default of 0 is the safer conservative fallback, consistent with how the other confidence/quality fields default to their "low" / zero values.

🔧 Proposed fix
-      completenessScore: toNum(results?.quality?.completenessScore, 100),
+      completenessScore: toNum(results?.quality?.completenessScore, 0),

Also update the fallback object on line 68:

-      completenessScore: 100,
+      completenessScore: 0,
costlens/server/server.js-83-92 (1)

83-92: ⚠️ Potential issue | 🟡 Minor

Health endpoint exposes missing environment variable names.

missingEnv (Line 90) reveals which API keys are absent. In production, this helps attackers understand the system's dependencies. Consider omitting the names and only returning the boolean envReady.

costlens/client/src/views/BuyerView.jsx-68-68 (1)

68-68: ⚠️ Potential issue | 🟡 Minor

Unguarded property accesses may throw if normalization produces unexpected shapes.

Several .length accesses assume arrays are always present without optional chaining, while earlier lines (e.g., Line 10, 15, 20) defensively use ?.. If a plan lacks hiddenCosts or gotchas, or if report.buyerCost.plans / tcoComparison / competitorComparison is undefined, these lines will throw a TypeError.

Examples: report.buyerCost.plans.length (Line 68, 82), plan.hiddenCosts.length (Line 97), plan.gotchas.length (Line 114), report.buyerCost.tcoComparison.length (Line 128), report.buyerCost.competitorComparison.length (Line 141).

Consider using optional chaining consistently, e.g. report.buyerCost?.plans?.length and plan.hiddenCosts?.length, or add fallbacks like (plan.hiddenCosts || []).

Also applies to: 82-82, 97-97, 114-114, 128-128, 141-141

costlens/server/services/build-cost-estimator.js-44-56 (1)

44-56: ⚠️ Potential issue | 🟡 Minor

domain.split(".")[0] may extract a subdomain prefix instead of the company name.

For URLs like app.example.com, hostname.split(".")[0] yields "app", not "example". This affects the TinyFish goal prompt. This is a known limitation shared with similar logic in server.js (Line 255) and getHiringCosts (Line 60).

costlens/server/server.js-906-911 (1)

906-911: ⚠️ Potential issue | 🟡 Minor

Error handler leaks allowed origins in CORS error responses.

originHint (Line 908) exposes the configured allowed origins to the caller. This reveals internal deployment URLs to potential attackers probing for CORS misconfigurations.

Proposed fix — omit the hint
- const originHint = [...getAllowedProdOrigins()];
- if (status < 500 && originHint.length > 0 && error?.message?.includes("CORS")) {
-   payload.allowedOrigins = originHint;
- }
costlens/server/services/build-cost-estimator.js-19-23 (1)

19-23: ⚠️ Potential issue | 🟡 Minor

Type mismatch: features fallback is [] but expected shape is an object.

When detectFeatures succeeds it returns { detected: [...], pricingPageFeatures: [...] }. On failure, Line 20 falls back to [] (an empty array). Downstream code (e.g., result.features.detected in buildMeta at Line 81, and the cost modeler's prompt serialization) expects an object, not an array. While this doesn't crash (optional chaining in buildMeta catches it), the semantic mismatch means metadata will silently report no features even if partial data was available.

Proposed fix
    const result = {
-     features: results[0]?.status === "fulfilled" ? results[0].value : [],
+     features: results[0]?.status === "fulfilled" ? results[0].value : { detected: [], pricingPageFeatures: [] },
      openSource: !fast && results[1]?.status === "fulfilled" ? results[1].value : [],
      hiring: !fast && results[2]?.status === "fulfilled" ? results[2].value : null,
    };
costlens/client/src/views/CompetitorView.jsx-38-68 (1)

38-68: ⚠️ Potential issue | 🟡 Minor

Competitor dots access c.positioning without a guard.

c.positioning.featureRichness and c.positioning.priceLevel (Lines 39-40) will throw if a competitor object is missing the positioning property. While the normalizer currently guarantees it, a defensive default would be safer:

- const cx = toX(c.positioning.featureRichness);
- const cy = toY(c.positioning.priceLevel);
+ const cx = toX(c.positioning?.featureRichness ?? 3);
+ const cy = toY(c.positioning?.priceLevel ?? 3);
costlens/server/analysis/cost-modeler.js-137-141 (1)

137-141: ⚠️ Potential issue | 🟡 Minor

reviewInsights fallback is {} but an array is expected.

Line 139 falls back to {} for reviewInsights, but the data comes from BuyerCostAnalyzer.scan() which returns it as an array. The system prompt will serialize {} as "{}" instead of "[]", giving the model misleading context.

Proposed fix
- Review Insights: ${JSON.stringify(data?.reviewInsights || {})}
+ Review Insights: ${JSON.stringify(data?.reviewInsights || [])}
🧹 Nitpick comments (32)
costlens/client/src/components/InfraVisualizations.jsx (2)

1-31: Hardcoded color values duplicate design tokens.

Colors like "#C41E3A", "#E8E4DF", "#8B8680", "#1A1815", and "#5C5650" are all defined in costlens/client/src/styles/tokens.js (accent, border, textMuted, text, textSecondary). Importing and referencing colors from tokens would keep the palette consistent and easier to update.

♻️ Suggested import
+import { colors } from "../styles/tokens";
+
 export function MarginGauge({ low, mid, high }) {

Then replace literals like "#C41E3A"colors.accent, "#E8E4DF"colors.border, etc.


38-66: Filter is applied twice on the same array.

items.filter((i) => i.pct > 0) is evaluated identically on Lines 38 and 60. Extracting it to a variable avoids the duplication and extra iteration.

♻️ Suggested refactor
 export function CostBar({ items = [] }) {
   const colors = ["#C41E3A", "#D4574A", "#E8845C", "#F2A86B", "#F5C97A", "#8B7355", "#6B8F71", "#4A7A8C"];
+  const visible = items.filter((i) => i.pct > 0);
   return (
     <div>
       <div style={{ display: "flex", height: 28, borderRadius: 4, overflow: "hidden", gap: 1, marginBottom: 10 }}>
-        {items.filter((i) => i.pct > 0).map((item, i) => (
+        {visible.map((item, i) => (
           ...
         ))}
       </div>
       <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
-        {items.filter((i) => i.pct > 0).map((item, i) => (
+        {visible.map((item, i) => (
           ...
         ))}
       </div>
costlens/package.json (1)

1-31: No package-lock.json or lockfile visible in this PR.

For reproducible builds, especially with all ^ ranges, consider committing a lockfile. This is less critical for a cookbook/demo project but good practice.

costlens/vercel.json (1)

7-7: Health endpoint maxDuration of 120s is unnecessarily generous.

A health check should respond in milliseconds. Consider lowering to something like 10–15s to match its expected behavior and avoid masking slowness.

Proposed fix
-    "api/health.js": { "maxDuration": 120 },
+    "api/health.js": { "maxDuration": 10 },
costlens/README.md (1)

93-96: Add a language specifier to the fenced code block.

The markdownlint MD040 rule flags this block for missing a language identifier. Adding one (e.g., ini or bash) improves rendering and accessibility.

Proposed fix
-```
+```ini
 TINYFISH_API_KEY=your_tinyfish_api_key
 OPENAI_API_KEY=your_openai_api_key
```
costlens/client/src/components/ui/WhyHint.jsx (2)

61-84: Consider adding ARIA attributes to the popup for better screen reader support.

The popup <div> lacks a role and id linkage. Adding role="tooltip" (or "status") with an id, and linking it from the button via aria-describedby, would let assistive technologies announce the popup content.


10-15: Resize listener fires on every pixel — consider debouncing.

setIsMobile is called on every resize event. While React batches updates, a lightweight debounce or matchMedia listener would be more efficient and idiomatic.

matchMedia alternative
  useEffect(() => {
-   const updateIsMobile = () => setIsMobile(window.innerWidth < 768);
-   updateIsMobile();
-   window.addEventListener("resize", updateIsMobile);
-   return () => window.removeEventListener("resize", updateIsMobile);
+   const mql = window.matchMedia("(max-width: 767px)");
+   setIsMobile(mql.matches);
+   const handler = (e) => setIsMobile(e.matches);
+   mql.addEventListener("change", handler);
+   return () => mql.removeEventListener("change", handler);
  }, []);
costlens/server/tinyfish/tinyfish-web-agent-client.js (2)

28-52: Consider extracting API-key validation and payload construction to reduce duplication.

runAutomation, runSync, and runAsync all repeat the same API key check and near-identical payload construction (Lines 29-37, 60-68, 73-81). A small private helper would eliminate this repetition.


255-259: 400 (Bad Request) and 422 (Unprocessable Entity) are retried, which is typically wasteful.

_isRetryable only excludes 401, 403, and 404. Client errors like 400 and 422 are deterministic and won't succeed on retry. Consider also returning false for the 4xx range (except 408/429).

Suggested fix
  _isRetryable(error) {
    const statusCode = Number(error?.statusCode);
    if (statusCode === 401 || statusCode === 403 || statusCode === 404) return false;
+   if (statusCode === 400 || statusCode === 422) return false;
    return true;
  }
costlens/server/services/infra-cost-scanner.js (1)

50-143: URL normalization logic is duplicated across multiple methods.

The pattern url.startsWith("http") ? url : \https://${url}`andnew URL(…).hostnameappears indetectTechStackAndTraffic, estimateTraffic, getEngineeringHeadcount, and detectThirdPartyServices(implicitly via the passed URL). Consider extracting a small_normalizeUrl(url)` helper.

costlens/server/services/buyer-cost-analyzer.js (1)

91-94: _coerceObject signature differs from InfraCostScanner._coerceObject — consider unifying.

Here it accepts a fallback parameter (Line 91), while in infra-cost-scanner.js it always returns {}. If these scanners share a common pattern, a shared utility would reduce drift.

costlens/client/src/components/ui/SectionLabel.jsx (1)

12-12: Avoid arithmetic on design tokens.

space.sm + 2 (= 10) breaks the token abstraction — if the spacing scale changes, this ad-hoc offset won't track. Consider using space.md (12) instead, or adding a dedicated token if 10px is intentional.

costlens/client/src/components/ExportBar.jsx (2)

54-56: Use colors.success token instead of hardcoded "#2E7D32".

The success color is already defined in your tokens (colors.success: "#2E7D32"), but Lines 54–56 hardcode it. This defeats the purpose of the token system.

♻️ Proposed fix
-          background: copied ? "#2E7D32" : colors.surface,
-          color: copied ? "#fff" : colors.textSecondary,
-          border: `1px solid ${copied ? "#2E7D32" : colors.border}`,
+          background: copied ? colors.success : colors.surface,
+          color: copied ? "#fff" : colors.textSecondary,
+          border: `1px solid ${copied ? colors.success : colors.border}`,

8-14: No user feedback on copy failure.

If copyTextSummary returns false, the button silently does nothing. Consider a brief error indication (e.g., a "Failed" state) so users know the clipboard write didn't succeed.

costlens/client/src/components/Badges.jsx (1)

1-49: Consider extracting shared badge styles and reusing tokens where applicable.

Both badge components share identical inline style properties (padding, borderRadius, fontFamily, fontSize, fontWeight, letterSpacing). A shared base style object would reduce duplication. Additionally, some hardcoded colors like "#2E7D32" and "#C41E3A" already exist as colors.success and colors.accent in your tokens.

costlens/client/src/components/ScanOverlay.jsx (1)

18-18: Inline <style> tag re-injected on every render.

The keyframe definitions are injected into the DOM each time the component renders. Consider extracting them to a shared CSS file or using a useEffect/useInsertionEffect to inject once.

costlens/client/src/utils/export.js (1)

118-121: Duplicate fmt helper — a different version already exists in formatting.js.

This file defines a local fmt with toLocaleString behavior, while costlens/client/src/utils/formatting.js exports an fmt with $K/$M shorthand. Having two divergent formatters named fmt risks confusion. Consider importing and reusing the shared one, or renaming this local variant (e.g., fmtPlain) to clarify intent.

costlens/client/src/components/ScanHistory.jsx (1)

31-46: Consider a confirmation step before clearing all history.

handleClear immediately wipes all saved reports with no undo. A window.confirm or similar prompt would prevent accidental data loss.

costlens/client/src/views/BuildView.jsx (1)

16-29: Inconsistent null-safety on report.buildCost access.

Lines 13–14 guard with report.buildCost?.confidence, but lines 19 and 22–29 access report.buildCost.validationWarnings, report.buildCost.totalEstimate, etc. directly. If report ever arrives un-normalized (e.g., from a stale localStorage entry or future code path), these lines will throw.

Pick one style: either trust normalizeReport and drop the ?. on lines 13–14, or add ?. consistently everywhere. The latter is safer.

costlens/server/test/api.test.js (1)

86-89: Test script may hang if the server keeps connections alive.

supertest creates an ephemeral server, but if app binds listeners or timers internally, the Node process may not exit after run() completes. Consider calling process.exit(0) on success as well.

🔧 Proposed fix
+  process.exit(0);
 }
 
 run().catch((err) => {
costlens/client/src/utils/report.js (1)

353-500: Massive duplication — the per-pillar qualityMeta normalization is copy-pasted 5×.

pillarCoverage, sourceCoverage, dataFreshness, confidenceScore, and perPillar each repeat an identical block for infra/build/buyer/risk/competitors. This is ~150 lines that can collapse to ~30 with a helper + loop.

♻️ Example refactor sketch
+const PILLARS = ["infra", "build", "buyer", "risk", "competitors"];
+
+function normalizePillarMap(root, perKey) {
+  return Object.fromEntries(PILLARS.map((p) => [p, perKey(root?.[p])]));
+}
+
 // then in normalizeReport:
-        pillarCoverage: {
-          infra: {
-            tasksSucceeded: toNum(results?.quality?.qualityMeta?.pillarCoverage?.infra?.tasksSucceeded),
-            tasksExpected: toNum(results?.quality?.qualityMeta?.pillarCoverage?.infra?.tasksExpected),
-          },
-          build: { /* ... identical ... */ },
-          buyer: { /* ... identical ... */ },
-          risk:  { /* ... identical ... */ },
-          competitors: { /* ... identical ... */ },
-        },
+        pillarCoverage: normalizePillarMap(
+          results?.quality?.qualityMeta?.pillarCoverage,
+          (v) => ({ tasksSucceeded: toNum(v?.tasksSucceeded), tasksExpected: toNum(v?.tasksExpected) })
+        ),

Apply the same pattern to sourceCoverage, dataFreshness, confidenceScore, and perPillar.

costlens/client/src/utils/history.js (1)

27-44: Storing full report blobs in localStorage risks hitting the ~5MB quota.

Each fullReport can be deeply nested (infra, build, buyer, risk, competitor, quality, provenance). With 10 entries, serialized size could exceed the browser's localStorage limit. The silent catch in writeStore means the save just disappears with no user feedback.

Consider either compressing before storing, storing only the report id and re-fetching from the server, or at minimum surfacing a warning when the write fails.

costlens/client/src/views/InfraView.jsx (2)

132-148: Hardcoded border color #F0EDE8 instead of design token.

Line 139 uses a raw hex value for the signal divider while the rest of the component uses colors.border (#E8E4DF). This may be intentional for a lighter separator, but it diverges from the token system and will be missed if the theme changes.

🔧 Proposed fix
-            borderBottom: i < signals.length - 1 ? "1px solid `#F0EDE8`" : "none",
+            borderBottom: i < signals.length - 1 ? `1px solid ${colors.border}` : "none",

13-23: Same optional chaining inconsistency as BuildView.

Lines 14–15 access report.infraCost.breakdown and report.infraCost.signals directly, while lines 17–18 guard report.infraCost?.confidence. Be consistent — either trust normalizeReport throughout or use ?. everywhere.

costlens/client/src/App.jsx (3)

36-49: Dead code: actions array is never used.

The actions array defined here is never referenced anywhere in this component. The scan progress messages are set via hardcoded strings in runScan (lines 92, 107, 127, 132, 143) and from server-side progress events — not from this array.

Proposed fix — remove the unused array
-  const actions = [
-    "Fingerprinting tech stack via HTTP headers...",
-    "Extracting client-side JavaScript bundles...",
-    "Detecting cloud provider from CDN signatures...",
-    "Mapping API endpoints and database patterns...",
-    "Crawling Cloudflare Radar for traffic estimates...",
-    "Scanning LinkedIn for engineering headcount...",
-    "Extracting salary data from Glassdoor...",
-    "Analyzing pricing page for additional costs...",
-    "Cross-referencing G2 reviews for overage complaints...",
-    "Checking Crunchbase for revenue estimates...",
-    "Estimating infrastructure costs via AWS Calculator...",
-    "AI synthesizing cost model...",
-  ];
-

192-192: @import inside <style> blocks render progress.

Loading Google Fonts via @import inside a <style> tag is render-blocking — the browser must fetch the CSS before painting. Move this to a <link> tag in your index.html for parallel loading.


55-81: Parameter url shadows the component-level state variable url.

The url parameter on Line 55 shadows the url state from Line 25. This works correctly since the parameter is what's needed inside the function, but it could confuse future readers. Consider renaming the parameter to requestUrl or endpoint.

costlens/client/src/views/CompetitorView.jsx (1)

236-298: Fixed two-column layout will break on narrow viewports.

The gridTemplateColumns: "1fr 1fr" at Line 236 combined with the fixed MAP_SIZE = 320 means the map panel alone needs ~320 px + padding. On mobile or narrow windows the grid won't wrap, causing horizontal overflow.

Consider using repeat(auto-fit, minmax(340px, 1fr)) or a media-query/container-query to stack the panels.

costlens/client/src/components/ReportSummary.jsx (2)

10-10: Pillar-label mapping is duplicated.

The pillar → label ternary chain appears on both Line 10 and Line 32. Extract a small helper or lookup object to stay DRY:

const PILLAR_LABELS = { infra: "Their Cost", build: "Build Cost", buyer: "Your Cost", risk: "Risk" };

Also applies to: 32-32


40-47: Deeply nested ternary is hard to scan.

Four levels of ternary nesting for headlineText hurts readability. A small lookup or switch would be clearer and easier to extend when new pillars are added.

costlens/server/server.js (1)

309-311: Timeout rejects with a plain object instead of an Error.

rej({ _timeout: true }) creates an unhandled-rejection without a stack trace. Use new Error("Investigation timeout") with a _timeout property for better debuggability:

- const timeoutP = new Promise((_, rej) =>
-   setTimeout(() => rej({ _timeout: true }), timeoutMs)
+ const timeoutP = new Promise((_, rej) =>
+   setTimeout(() => { const e = new Error("Investigation timeout"); e._timeout = true; rej(e); }, timeoutMs)
  );
costlens/server/analysis/cost-modeler.js (1)

150-174: No backoff between retries in requestJsonWithRetry.

The retry loop immediately re-invokes the OpenAI API on failure. If the failure is a rate-limit (429) or transient server error, instant retries are likely to fail again and burn through the retry budget. Add a short exponential delay:

Proposed fix
    for (let attempt = 0; attempt <= retries; attempt++) {
      try {
        const response = await this.openai.chat.completions.create({
          ...
        });
        ...
        return JSON.parse(content);
      } catch (error) {
        lastError = error;
+       if (attempt < retries) {
+         await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
+       }
      }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant