Your dashboard says a product has thousands of reviews. You trace the tile back to the review rows you just imported, and the arithmetic looks perfectly respectable.

The unit is wrong. A review's totalCount measures combined upvotes and downvotes, not the product's review total.

That mistake survives successful jobs, valid JSON, and tidy charts. Before you pay for another collection run, follow the field feeding the tile.

Example collection input below makes the review settings explicit and retrieves items after the run finishes:

import { ApifyClient } from 'apify-client'; import { z } from 'zod'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const input = { mode: 'search', searchQuery: 'laptop', includeDetails: true, includeReviews: true, maxReviewsPerProduct: 20, reviewsSort: 'MOST_HELPFUL', certifiedBuyersOnly: false, maxItems: 3, maxPages: 1, }; const run = await client .actor('nimbryxo-studio/flipkart-product-price-and-specs-scraper') .call(input); const { items } = await client .dataset(run.defaultDatasetId) .listItems(); 

All collection counts below describe the fetched items. For larger datasets, retrieve every page before calling those counts run totals.

Follow the field feeding the symptom

The marketplace exposes different feedback activities. Leaving a star rating, writing a review, and voting on someone else's review produce different counts.

Your dashboard needs to preserve those boundaries.

Symptom Distinguishing evidence Easy misdiagnosis Fix and cost
Product review volume comes from review rows Metric reads review totalCount Duplicate ingestion Read product reviewCount; rebuild affected aggregates
Written-review coverage looks unexpectedly low Denominator reads product ratingCount Missing review pages Use reviewCount for written-review coverage; recompute historical ratios
Collected rows fall below the reported review count Compare row count with the configured cap and certified-buyer scope Collection failure Keep the collection scope visible; expanding it requires another run
A product feedback count is absent Product record omits the field Product has no feedback Preserve an unknown value; accept an incomplete metric
Vote figures disagree Present upvotes plus downvotes differ from present totalCount Broken review pagination Preserve the disagreement for inspection; avoid automatic correction

The distinction between collection failure and semantic failure matters. Another scrape can't repair a formula that counts the wrong activity.

Validate the record before the number

Products and reviews share fields such as rating and title. Classify them through recordType, rather than guessing from whichever fields happen to be present.

Empty fields are omitted. Making counts optional preserves that behavior without turning missing feedback into zero feedback.

This validator keeps the fields needed for diagnosis and separates rejected items from usable records:

const count = z.number().int().nonnegative().optional(); const Product = z.object({ recordType: z.literal('product'), productId: z.string().min(1), ratingCount: count, reviewCount: count, }); const Review = z.object({ recordType: z.literal('review'), productId: z.string().min(1), reviewId: z.string().optional(), totalCount: count, helpfulCount: count, upvotes: count, downvotes: count, }); const Item = z.discriminatedUnion('recordType', [Product, Review]); const records: z.infer<typeof Item>[] = []; const rejected: { index: number; reason: string }[] = []; items.forEach((item, index) => { const parsed = Item.safeParse(item); if (parsed.success) records.push(parsed.data); else rejected.push({ index, reason: parsed.error.message }); }); const products = records.filter(r => r.recordType === 'product'); const reviews = records.filter(r => r.recordType === 'review'); 

A rejected row is a validation problem. An accepted product without reviewCount is an unavailable measurement. Keep those diagnoses separate.

Otherwise, a schema change and an absent source value can both become a reassuring zero.

Is your review denominator counting votes?

Values alone can't establish this error. A vote count can look entirely plausible as a review count.

You need the metric definition alongside the records. The useful question is which record type and field supplied the denominator.

Keep that lineage in application-owned configuration. The following deliberately incorrect definitions demonstrate the detector without throwing an exception.

Example dashboard definitions below declare their intended unit alongside their actual source:

const metricDefinitions = [ { name: 'writtenReviewVolume', expectedUnit: 'written-reviews', recordType: 'review', field: 'totalCount', }, { name: 'collectedReviewCoverageDenominator', expectedUnit: 'written-reviews', recordType: 'product', field: 'ratingCount', }, ]; const sourceUnits = new Map([ ['product.reviewCount', 'written-reviews'], ['product.ratingCount', 'star-ratings'], ['review.totalCount', 'review-votes'], ]); const metricRepairs = metricDefinitions.flatMap(metric => { const source = `${metric.recordType}.${metric.field}`; const actualUnit = sourceUnits.get(source); if (actualUnit === metric.expectedUnit) return []; return [{ metric: metric.name, source, actualUnit: actualUnit ?? 'unmapped', expectedUnit: metric.expectedUnit, action: 'Review the mapping, then recompute dependent aggregates', }]; }); 

The definition provides intent; the source map provides meaning. Their disagreement identifies the repair.

For written-review volume, use the product count. For written-review coverage, divide collected written-review rows by the reported written-review count, provided that denominator exists and is positive.

A written-reviews-per-star-rating ratio can also be useful. Give it that name. The mistake is silently presenting it as collection coverage.

Count the rows you actually collected

Your imported rows describe collection output. The product's feedback counters describe reported activity on the marketplace.

Those are different populations:

product record ratingCount -> reported star ratings reviewCount -> reported written reviews collected review records row count -> imported review rows totalCount -> votes attached to each collected review 

The review cap limits collection per product. A certified-buyer filter narrows eligible reviews. Neither changes what the product's written-review count means.

This aggregation keeps the measurements separate and records collection boundaries:

type ReviewRow = z.infer<typeof Review>; const reviewsByProduct = new Map<string, ReviewRow[]>(); for (const review of reviews) { const group = reviewsByProduct.get(review.productId) ?? []; group.push(review); reviewsByProduct.set(review.productId, group); } const metrics = products.map(product => { const rows = reviewsByProduct.get(product.productId) ?? []; const votes = rows.flatMap(row => row.totalCount === undefined ? [] : [row.totalCount] ); return { productId: product.productId, reportedStarRatings: product.ratingCount ?? null, reportedWrittenReviews: product.reviewCount ?? null, collectedReviewRows: rows.length, rowsWithReportedVotes: votes.length, reportedReviewVotesOnCollectedRows: votes.length === rows.length ? votes.reduce((sum, value) => sum + value, 0) : null, missingProductCounts: product.ratingCount === undefined || product.reviewCount === undefined, collectionFlags: { reviewsDisabled: !input.includeReviews, zeroReviewBudget: input.maxReviewsPerProduct === 0, certifiedBuyerScope: input.certifiedBuyersOnly, atConfiguredCap: input.includeReviews && input.maxReviewsPerProduct > 0 && rows.length === input.maxReviewsPerProduct, noCollectedReviewRows: rows.length === 0, }, }; }); 

Reaching the configured boundary is consistent with the cap limiting collection. It doesn't prove additional reviews were available.

Likewise, a certified-buyer setting establishes scope, but the returned rows can't reveal how many other reviews were excluded.

Below the cap, a shortfall remains unresolved from these counters alone. Avoid promoting “fewer rows than reported reviews” into a confirmed ingestion failure.

Increasing the cap may expand your analysis, with additional result-event charges and platform usage. It still does nothing for a denominator mapping error.

Treat vote disagreements as evidence

totalCount carries the combined vote figure reported by the source. When both components exist, their sum gives you a useful consistency check.

That check has a narrow job. It doesn't establish which value should win.

The following detector retains all available vote fields, including helpfulCount, without assuming that helpfulness means total voting activity:

const voteChecks = reviews.map(review => { const comparable = review.upvotes !== undefined && review.downvotes !== undefined && review.totalCount !== undefined; const componentSum = comparable ? review.upvotes! + review.downvotes! : null; return { productId: review.productId, reviewId: review.reviewId ?? null, helpfulCount: review.helpfulCount ?? null, upvotes: review.upvotes ?? null, downvotes: review.downvotes ?? null, reportedTotal: review.totalCount ?? null, componentSum, diagnosis: !comparable ? 'insufficient-vote-fields' : componentSum === review.totalCount ? 'components-agree' : 'vote-discrepancy', }; }); 

An omitted component can't support an arithmetic comparison. A differing helpfulCount alone also doesn't establish corruption, because its exact relationship to the other counters isn't specified.

Keep the raw values. Replacing the reported total with your calculation would erase the evidence you need to investigate.

Helpfulness also measures voting behavior, not sentiment. Weighting a sentiment average by votes changes the question toward highly voted reviews. That may be intentional, but it needs its own metric definition.

A successful run can't certify a metric

The worked example uses Flipkart Product Price & Specs Scraper.

Its run status tells you about execution. It can't tell you whether your dashboard assigned the right unit to a field.

There's another boundary: if product detail enrichment fails after retries, the listing record can still be returned. Enrichment failures are reported in the run status information. Missing fields alone can't identify that fallback, because unpublished values are also omitted.

This report keeps execution, validation, metric mappings, and row evidence available separately:

const repairReport = { runStatus: run.status, acceptedRows: records.length, rejectedRows: rejected, metricRepairs, metrics, voteChecks, }; 

Use the mapping findings to choose recomputation work. Use rejected rows and execution evidence to investigate collection or parsing.

For stored aggregates, changing the dashboard label is insufficient. Trace affected calculations back to their source fields and rebuild them from retained records. If the required product counters were never retained, recovering that measurement may require fresh collection.

Before you refresh the dashboard

  • Confirm the execution status and the scope of fetched items.
  • Inspect rejected records before trusting collection counts.
  • Trace each denominator to its record type and field.
  • Keep absent counters unknown.
  • Check cap and certified-buyer scope before diagnosing a shortfall.
  • Preserve vote discrepancies, then rebuild affected aggregates.

A plausible number can still be counting the wrong thing.