feat(geo): AI visibility tracking across AI engines - #657
Conversation
Tracks how AI engines mention your brand: prompt scanning with grounded engines, competitor share of voice, and the @usenotra/geo SDK with server-side visitor classification, journey attribution and markdown link tagging.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Comp AI code review failed. Comment Commit |
|
React Doctor found 67 new issues in 38 files · 3 errors & 64 warnings · score 49 / 100 (Critical) · 0 fixed · vs Errors
64 warnings
14 more warnings not shown. Reviewed by React Doctor for commit |
…bility # Conflicts: # packages/analytics/src/tinybird/endpoints.ts
There was a problem hiding this comment.
No blocking issues found across the changed files.
Commit 4f970c5 · Posted by Comp AI Code Reviews.
Greptile SummaryAdds end-to-end GEO visibility tracking, including project configuration, AI-engine scans, traffic ingestion, journey attribution, dashboards, and a reusable tracking package.
Confidence Score: 3/5The PR should not merge until project-scoped ingest credentials are validated and duplicate GEO scans are prevented. Invalid project identifiers can be embedded in working ingest tokens and produce unreachable analytics data, while concurrent scan starts independently execute billable checks and append duplicate metric rows. Files Needing Attention: apps/dashboard/src/lib/orpc/routers/geo.ts, apps/dashboard/src/lib/workflows/start.ts, apps/dashboard/src/lib/geo/scan.ts Important Files Changed
Sequence DiagramsequenceDiagram
participant User as Dashboard user
participant RPC as GEO oRPC
participant Workflow as GEO scan workflow
participant Engines as AI engines
participant Tinybird as Analytics ingest
User->>RPC: Start project scan
RPC->>Workflow: startGeoScanRun
Workflow->>Engines: Run prompt checks
Engines-->>Workflow: Responses and mentions
Workflow->>Tinybird: Append mention-check rows
User->>RPC: Load GEO dashboard
RPC->>Tinybird: Query project metrics
Tinybird-->>RPC: Visibility, traffic, and journeys
Reviews (1): Last reviewed commit: "merge: drop analytics backfill script" | Re-trigger Greptile |
|
|
||
| return { | ||
| ingestUrl: buildGeoIngestUrl(), | ||
| token: buildGeoIngestToken(input.organizationId, input.projectId) ?? "", |
There was a problem hiding this comment.
Unvalidated project-scoped ingest tokens
When an organization member requests ingest setup with a stale, fabricated, or foreign projectId, this handler signs that value without resolving it against the organization. Subsequent events are accepted under an invalid organization/project pairing but cannot be retrieved through project-scoped dashboard queries, contaminating the analytics stream with unreachable data.
Knowledge Base Used: Dashboard App Core Structure
| export async function startGeoScanRun(payload: { | ||
| organizationId: string; | ||
| projectId?: string; | ||
| }): Promise<{ runId: string }> { | ||
| const parsed = geoOrganizationInputSchema.parse(payload); | ||
| const run = await start(geoScanWorkflow, [parsed]); | ||
| return { runId: run.runId }; | ||
| } |
There was a problem hiding this comment.
Non-idempotent project scan dispatch
If two scan requests for the same project overlap or the workflow endpoint receives a duplicate delivery, each invocation starts an independent workflow with a fresh scan ID. Both runs execute the same billable AI checks and append separate rows to the non-deduplicating datasource, causing duplicate spend and double-counted visibility metrics.
|
|
||
| useEffect(() => { | ||
| if (!ready) { | ||
| setStage(0); |
There was a problem hiding this comment.
React Doctor · react-hooks-js/set-state-in-effect (warning)
This synchronous effect update causes an extra render: Calling setState synchronously within an effect can trigger cascading renders. Prefer deriving or initializing the value before render. If the effect must read a browser API after mount, treat this as advisory or suppress it with // react-doctor-disable-next-line react-hooks-js/set-state-in-effect.
Fix → Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
- Update external systems with the latest state from React.
- Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
| "use client"; | ||
|
|
||
| import type { Transition } from "motion/react"; | ||
| import { motion, useTransform } from "motion/react"; |
There was a problem hiding this comment.
React Doctor · react-doctor/use-lazy-motion (warning)
Importing "motion" ships about 30 kb of extra code and slows page load. Use "m" with LazyMotion instead.
Fix → Use import { LazyMotion, m } from "framer-motion" with domAnimation features. Saves about 30kb.
|
|
||
| // ─── Label overlay ────────────────────────────────────────────────── | ||
|
|
||
| function SegmentLabel({ |
There was a problem hiding this comment.
React Doctor · react-doctor/no-many-boolean-props (warning)
Component "SegmentLabel" takes 4 on/off props (isHorizontal, showValues, showPercentage…), which is hard to combine & test. Split it into smaller components or named variants.
Fix → Split boolean-heavy APIs into smaller components or named variants so combinations stay testable.
|
|
||
| const isControlled = hoveredIndexProp !== undefined; | ||
| const hoveredIndex = isControlled ? hoveredIndexProp : internalHoveredIndex; | ||
| const setHoveredIndex = useCallback( |
There was a problem hiding this comment.
React Doctor · react-doctor/react-compiler-no-manual-memoization (warning)
React Compiler can cache this function automatically. Verify that removing useCallback preserves behavior before simplifying it.
Fix → Profile compiler-managed code and remove useMemo, useCallback, or memo only when the manual cache no longer carries behavioral or performance intent.
| [isControlled, onHoverChange] | ||
| ); | ||
|
|
||
| const measure = useCallback(() => { |
There was a problem hiding this comment.
React Doctor · react-doctor/react-compiler-no-manual-memoization (warning)
React Compiler can cache this function automatically. Verify that removing useCallback preserves behavior before simplifying it.
Fix → Profile compiler-managed code and remove useMemo, useCallback, or memo only when the manual cache no longer carries behavioral or performance intent.
| } | ||
|
|
||
| export function MentionRateCard({ engines }: MentionRateCardProps) { | ||
| const families = useMemo(() => groupEngines(engines), [engines]); |
There was a problem hiding this comment.
React Doctor · react-doctor/react-compiler-no-manual-memoization (warning)
React Compiler can cache this value automatically. Verify that removing useMemo preserves behavior before simplifying it.
Fix → Profile compiler-managed code and remove useMemo, useCallback, or memo only when the manual cache no longer carries behavioral or performance intent.
| import { buildMentionRateRows } from "@/utils/geo-charts"; | ||
|
|
||
| export function MentionTrendCard({ points }: MentionTrendCardProps) { | ||
| const { rows, engines } = useMemo( |
There was a problem hiding this comment.
React Doctor · react-doctor/react-compiler-no-manual-memoization (warning)
React Compiler can cache this value automatically. Verify that removing useMemo preserves behavior before simplifying it.
Fix → Profile compiler-managed code and remove useMemo, useCallback, or memo only when the manual cache no longer carries behavioral or performance intent.
| [points] | ||
| ); | ||
|
|
||
| const series = useMemo( |
There was a problem hiding this comment.
React Doctor · react-doctor/react-compiler-no-manual-memoization (warning)
React Compiler can cache this value automatically. Verify that removing useMemo preserves behavior before simplifying it.
Fix → Profile compiler-managed code and remove useMemo, useCallback, or memo only when the manual cache no longer carries behavioral or performance intent.
| [engines] | ||
| ); | ||
|
|
||
| const config = useMemo(() => { |
There was a problem hiding this comment.
React Doctor · react-doctor/react-compiler-no-manual-memoization (warning)
React Compiler can cache this value automatically. Verify that removing useMemo preserves behavior before simplifying it.
Fix → Profile compiler-managed code and remove useMemo, useCallback, or memo only when the manual cache no longer carries behavioral or performance intent.
| import { tableHeightFor } from "@/utils/table"; | ||
|
|
||
| export function ModelUsageCard({ usage }: ModelUsageCardProps) { | ||
| const models = useMemo(() => usage?.models ?? [], [usage]); |
There was a problem hiding this comment.
React Doctor · react-doctor/react-compiler-no-manual-memoization (warning)
React Compiler can cache this value automatically. Verify that removing useMemo preserves behavior before simplifying it.
Fix → Profile compiler-managed code and remove useMemo, useCallback, or memo only when the manual cache no longer carries behavioral or performance intent.
| const withInlineLinks = text.replace( | ||
| INLINE_LINK, | ||
| (match, bang: string, label: string, target: string, title: string) => { | ||
| if (bang === "!" || !isTaggableTarget(target, host)) { | ||
| return match; | ||
| } | ||
| return `[${label}](${appendJourneyParam(target, journeyId)}${title})`; | ||
| } | ||
| ); |
| const TRAILING_SLASHES = /\/+$/; | ||
|
|
||
| function ingestUrl(endpoint: string | undefined): string { | ||
| const base = (endpoint ?? DEFAULT_ENDPOINT).replace(TRAILING_SLASHES, ""); |
| export async function geoScanWorkflow( | ||
| payload: GeoScanPayload | ||
| ): Promise<GeoScanResult> { | ||
| "use workflow"; |
| organizationId: string, | ||
| projectId?: string | ||
| ): Promise<GeoScanResult> { | ||
| "use step"; |
| import { Loader2Icon } from "lucide-react"; | ||
| import { useState } from "react"; | ||
| import { CompetitorLogo } from "@/components/geo/competitor-logo"; | ||
| import { findCompetitorDomain } from "@/lib/geo/domain"; |
| }: GeoCompetitorsDialogProps) { | ||
| const [draft, setDraft] = useState(""); | ||
| const [domainDraft, setDomainDraft] = useState(""); | ||
| const [selected, setSelected] = useState<string | null>(null); |
| } from "@/types/geo-directions"; | ||
|
|
||
| const PERCENT = 100; | ||
| const MIN_BAR_PERCENT = 3; |
Description
Give a short summary of what this PR does and why it's needed.
Screenshot/Recording (if applicable)
Attach a screenshot or recording of the change. This is optional, but can help reviewers understand the change. You can use Cap to record a video.
Checklist
Summary by cubic
Adds GEO brand visibility tracking across AI engines with prompt scans, AI traffic ingest, journeys, and dashboards for share of voice, engine coverage, and trends. Integrates with the social analytics ClickHouse base and ships an ingest pipeline and Directions reports.
New pages & metrics: GEO Overview, Prompts, Competitors (page + modal), Directions (Instrument, Leaderboard, Cockpit, Report) with engine rates/trends, share of voice, prompt funnel, model/language usage, journeys, AI traffic log/pages.
Project scoping & settings: project switcher + context; settings for company, aliases, languages, competitors; onboarding from website.
Prompts: create prompts, run scans, and view per‑engine results; grounded (web) and raw engine checks with judging and excerpts; results preview.
Ingest & workflows: POST
/api/geo/ingestwith HMAC token auth, Effect‑based pipeline, visitor classification, and journey attribution; POST/api/workflows/geo-scanwith QStash signature verification; docs and a Next proxy snippet for@usenotra/geo.Data/API & UX: ORPC
georouter, React hooks, schemas, and mappers; GEO nav category and command palette routes; improved skeletons/tables; new funnel chart and chart animation/formatters.Dependencies: add
@ai-sdk/anthropic,@ai-sdk/openai,@ai-sdk/perplexity,@usenotra/geo,sugar-high,color; update Next transpile list to include@usenotra/geo.Refactors
knip.jsoncoverage; repaired analytics migration chain; dropped analytics backfill script.Bug Fixes
Written for commit 668067f. Summary will update on new commits.
Summary by Comp AI
No blocking issues found.
Written for commit
4f970c5. New commits will trigger a re-review. Generated by Comp AI.