If your Looker Studio report shows "Data Set Configuration Error" on charts that worked yesterday, it's usually not your data. It's how the report reads it.
Short answer: avoid blending your Google Sheet with Google Ads for the core numbers. Use standalone scorecards, or join the data inside the Sheet. After adding columns, open the data source and click Refresh fields. On the logging side, use LockService and widen the grid, or rows disappear without an error.
Our server logs every booking from a US salon's Boulevard tracking pipeline to a Google Sheet, and a Looker Studio report reads it live. The pipeline itself is in our complete server-side guide.
The setup
Server (Cloudflare Worker) ──POST row──▶ Apps Script web app ──▶ Google Sheet ──▶ Looker Studio
- Every conversion (booking or completed visit) becomes one row: time, appointment, event type, value, channel, UTMs, click IDs, match tier, and whether each ad platform accepted it.
- A small Apps Script web app (
doPost) receives the row and appends it. - Looker Studio connects to the Sheet: total bookings, bookings by channel, new clients, revenue and a table of recent bookings.
We also keep every row in the server's own storage for 90 days, so if the Sheet ever fails, nothing is lost.
The Apps Script logger, and the rows we lost
A logger looks trivial. Ours dropped rows twice before we fixed it.
// Apps Script: append one row per conversion, safely
var SHEET_NAME = ''; // '' = first sheet
var COLUMNS = ['ts', 'appointment_id', 'event_type', 'event_id', 'value', 'currency',
'channel', 'utm_source', 'utm_campaign', 'fbclid', 'gclid', 'ttclid',
'match_tier', 'capi_meta_ok', 'capi_google_ok', 'capi_tiktok_ok'];
function doPost(e) {
var lock = LockService.getScriptLock();
lock.waitLock(20000); // serialize concurrent writes
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = SHEET_NAME ? ss.getSheetByName(SHEET_NAME) : ss.getSheets()[0]; // never getActiveSheet()
if (sh.getMaxColumns() < COLUMNS.length) { // default grid is 26 columns wide
sh.insertColumnsAfter(sh.getMaxColumns(), COLUMNS.length - sh.getMaxColumns());
}
if (sh.getLastRow() === 0) sh.appendRow(COLUMNS);
var data = JSON.parse(e.postData.contents);
sh.appendRow(COLUMNS.map(function (k) { return data[k] !== undefined ? data[k] : ''; }));
return ContentService.createTextOutput('ok');
} finally {
lock.releaseLock();
}
}
What each line protects against:
LockService: bookings and their webhooks arrive seconds apart. Without a lock, two writes can collide.- Widening the grid: a new sheet is 26 columns wide. When we added a 27th field, reading or writing that column threw an error and the row was silently dropped.
- An explicit sheet instead of
getActiveSheet(), which isn't reliable when the script runs as a web app with nobody looking at it. - Mapping by column name, and adding new columns at the end, so old rows stay aligned with the header.
Redeploying without breaking the URL: in Apps Script, use Deploy → Manage deployments → edit the existing deployment → New version. That keeps the same web app URL. Creating a new deployment gives a new URL, and your server keeps posting to the old one.
"Data Set Configuration Error": what fixed it for us
Blends of the Sheet and Google Ads
We wanted ROAS on one chart: revenue from the Sheet, cost from Google Ads, blended by date. Those blends repeatedly showed Data Set Configuration Error, even when each source worked fine on its own.
What worked instead:
- Standalone scorecards. One scorecard with Google Ads cost (from the Google Ads connector), one with revenue (from the Sheet), side by side. It's less elegant and much more reliable.
- Join in the Sheet. Put weekly spend in its own tab and calculate ROAS there. Looker Studio then reads one clean source.
New columns break the data source
Looker Studio stores the list of fields when you connect the Sheet. After you add a column:
- Open Resource → Manage added data sources → Edit.
- Click Refresh fields.
- Check the new field's type (number vs text) before using it.
Until you do, charts that touch the changed source can fail.
Other causes worth checking
- A metric stored as text. If
valuewas ever written as"12.50"text, aggregation fails. Set the field type to Number in the data source. - Authorization. If the account that connected the data source lost access to the Sheet, every chart on it fails.
Getting ad spend in: the honest version
- Google Ads has a free native Looker Studio connector.
- Meta and TikTok don't. Either type weekly spend into a tab in the Sheet (five minutes a week), or pay for a connector.
For one business, the manual tab is fine and removes another integration that can break.
The columns that make the dashboard trustworthy
Beyond bookings and revenue, three columns turned our dashboard from a pretty chart into something we could act on:
match_tier: how each booking was matched to an ad click. The share of deterministic matches vs guesses is visible. We explain why in our match-by-IP case study.capi_*_okper platform: a column offalsefor TikTok or Google means a silent failure, spotted the same day.is_new_client: new clients from ads vs returning ones are very different results.
Common errors
- Data Set Configuration Error on blended charts: replace the blend with standalone scorecards, or compute ROAS in the Sheet.
- Charts broken after a Sheet change: Refresh fields in the data source.
- Totals wrong or missing: a number field typed as text.
- Rows missing in the Sheet: no LockService, or the 26-column limit.
- Server posting but no rows: you redeployed as a new deployment and the URL changed.
Testing checklist
- A test function appends a clearly named test row (then delete it).
- Two test bookings close together both appear.
- Every column, including the last one, is filled.
- The data source fields are refreshed and typed correctly.
- Spend and revenue scorecards match the platforms and the Sheet.
Related reading: Google Ads offline click conversions: gbraid errors and silent partial failures.

