How searchPerformance.ts Joins Google Search Console Data with Rank Tracking

The searchPerformance.ts server function aggregates parallel Google Search Console API requests to build a unified performance report, while the actual join with rank-tracking data occurs on the client side to maintain separation between free GSC metrics and paid DataForSEO queries.

Open SEO's analytics pipeline relies on src/serverFunctions/searchPerformance.ts to orchestrate data retrieval from Google Search Console. This server-side entry point handles complex GSC aggregation and filtering, but deliberately defers rank-tracking integration to the frontend to isolate expensive third-party API calls from Google's free, rate-limited service.

Aggregating GSC Data with Parallel API Calls

The getSearchPerformanceReport function executes four concurrent GscService.getPerformance requests to construct a complete search performance picture. This parallel architecture minimizes response latency while respecting GSC API constraints.

Building Shared Filters with buildGscFilters

Before issuing API calls, the helper buildGscFilters constructs dimension filters for device (desktop/mobile) and optional country selections. These filters ensure dimensional consistency across all subsequent GSC requests.

function buildGscFilters(data: { device?: string; country?: string }) {
  const deviceFilters = data.device
    ? [{ dimension: "device", operator: "equals", expression: data.device }]
    : [];
  const filters = data.country
    ? [...deviceFilters, { dimension: "country", operator: "equals", expression: data.country }]
    : deviceFilters;
  return { deviceFilters, filters };
}

The function returns both deviceFilters and combined filters to support different query requirements—countries list only needs device filtering, while performance breakdowns include both dimensions.

Fetching Current, Previous, and Breakdown Data

The server function fires four parallel requests through Promise.all to gather distinct metric slices:

  1. Current period (dimensions: ["date"]) - Daily totals for the selected date range
  2. Previous period (dimensions: ["date"]) - Prior-period totals for comparison calculations
  3. Query and page rows (dimensions: ["query", "page"]) - Granular breakdown for striking-distance keyword identification
  4. Country list (dimensions: ["country"]) - Available countries for the filter dropdown
const [current, previous, queryPages, countries] = await Promise.all([
  GscService.getPerformance({ projectId, startDate, endDate, dimensions: ["date"], filters, rowLimit: DAILY_ROW_LIMIT }),
  GscService.getPerformance({ projectId, startDate: prev.startDate, endDate: prev.endDate, dimensions: ["date"], filters, rowLimit: DAILY_ROW_LIMIT }),
  GscService.getPerformance({ projectId, startDate, endDate, dimensions: ["query", "page"], filters, rowLimit: STRIKING_DISTANCE_FETCH_LIMIT }),
  GscService.getPerformance({ projectId, startDate, endDate, dimensions: ["country"], filters: deviceFilters, rowLimit: COUNTRY_ROW_LIMIT })
]);

Each request uses the shared filter context to maintain consistency across time periods and dimensions.

Transforming Raw GSC Rows for the UI

After fetching raw data, helper functions from src/server/features/gsc/searchPerformanceReport.ts convert API responses into structures optimized for frontend consumption:

  • sumSearchTotals - Aggregates impressions, clicks, and CTR across date ranges
  • buildStrikingDistanceRows - Identifies high-opportunity keywords from query/page combinations
  • toDimensionRows - Formats country lists and other dimension-based data

These transformations occur server-side to reduce client-side computation and payload sizes.

Client-Side Integration with Rank Tracking

The searchPerformance.ts module does not directly query the rank-tracking service (src/shared/rank-tracking.ts). Instead, the joining occurs in the UI layer to maintain architectural separation between free GSC data and paid DataForSEO rank checking.

The client-side flow in SearchPerformancePage.tsx follows this sequence:

  1. Fetch GSC report via getSearchPerformanceReport
  2. Extract keywords from strikingDistance rows
  3. Query rank-tracking API separately for those keywords
  4. Merge datasets using matching query strings
// Client-side data fetching and joining
const report = await getSearchPerformanceReport({
  data: {
    projectId,
    dateRange: selectedRange,
    device: selectedDevice,
    country: selectedCountry,
  },
});

const rankData = await fetchRankTracking({
  projectId,
  keywords: report.strikingDistance.map(r => r.query),
  startDate: report.range.startDate,
  endDate: report.range.endDate,
});

const combined = report.strikingDistance.map(row => ({
  ...row,
  rank: rankData.find(r => r.keyword === row.query)?.position ?? null,
}));

This approach aligns GSC and rank data using shared dimensions (date, device, country) without blocking GSC retrieval on expensive rank-checking latency.

Architecture Benefits of Data Separation

Decoupling GSC aggregation from rank-tracking queries provides critical operational advantages. Google Search Console offers free API access with generous rate limits but delayed data, while DataForSEO rank tracking requires paid credits per keyword check. By keeping these services separate, Open SEO prevents rank-checking failures or timeouts from degrading core GSC reporting performance.

The server function exposes a clean GSC-only interface (getSearchPerformanceReport), while the frontend manages the complexity of merging external rank data with the shared filter context established by buildGscFilters.

Summary

  • searchPerformance.ts aggregates four parallel GSC requests (current period, previous period, query/page breakdown, and country list) using shared dimension filters
  • The buildGscFilters helper ensures consistent device and country filtering across all API calls
  • Server-side transformation utilities in src/server/features/gsc/searchPerformanceReport.ts prepare striking-distance keywords and aggregated totals
  • Rank-tracking data joins on the client side in SearchPerformancePage.tsx to isolate expensive DataForSEO queries from free GSC calls
  • This separation maintains fast GSC reporting while allowing flexible rank-data enrichment

Frequently Asked Questions

Does searchPerformance.ts directly query rank-tracking data?

No. According to the Open SEO source code, searchPerformance.ts strictly handles Google Search Console data retrieval. Rank-tracking integration occurs downstream in the UI components, which call separate rank-tracking endpoints and merge the results client-side using keyword matching.

Why are GSC and rank-tracking data kept separate rather than joined server-side?

The separation optimizes cost and performance. GSC provides free API access with rate limits, while DataForSEO rank checking charges per keyword query. Keeping these services decoupled prevents expensive or slow rank-checking operations from blocking or degrading the core GSC report performance.

What are striking-distance rows in the GSC report?

Striking-distance rows refer to high-opportunity keyword and page combinations extracted from the dimensions: ["query", "page"] GSC breakdown. These represent queries where the site ranks close to the first page but needs improvement, making them prime candidates for rank-tracking monitoring.

How does the client ensure GSC and rank-tracking data align correctly?

The client uses the shared filter context (device type, country, and date range) established by buildGscFilters when requesting both datasets. It then matches rank-tracking positions to GSC query strings, falling back to null values when rank data is unavailable for specific keywords.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →