{"record":{"id":"a534dd385727779c","repo":"Hmbown/CodeWhale","slug":"cloudflare-sql-response-did-not-contain-a-data-arr","errorCode":null,"errorMessage":"Cloudflare SQL response did not contain a data array","messagePattern":"Cloudflare SQL response did not contain a data array","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"telemetry-ingest/scripts/report-active-installs.mjs","lineNumber":79,"sourceCode":"WHERE timestamp >= toStartOfDay(NOW()) - INTERVAL '${days - 1}' DAY\n  AND blob1 = 'session_start'\nGROUP BY day\nORDER BY day DESC\nFORMAT JSON`;\n}\n\n/** Newest ingested event of any kind — how stale the dataset is. */\nexport function freshnessSql() {\n  return `SELECT\n  max(timestamp) AS newest_event\nFROM codewhale_telemetry\nFORMAT JSON`;\n}\n\nfunction dataRows(payload) {\n  const rows = Array.isArray(payload) ? payload : payload?.data;\n  if (!Array.isArray(rows)) {\n    throw new Error(\"Cloudflare SQL response did not contain a data array\");\n  }\n  return rows;\n}\n\nexport function rowsFromResponse(payload) {\n  return dataRows(payload).map((row) => ({\n    day: String(row.day),\n    active_installs: Number(row.active_installs),\n    sessions_started: Number(row.sessions_started),\n  }));\n}\n\n/** `null` when the dataset has no rows at all. */\nexport function newestEventFromResponse(payload) {\n  const raw = dataRows(payload)[0]?.newest_event;\n  if (raw === undefined || raw === null || String(raw).startsWith(\"0000\")) {\n    return null;\n  }","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/telemetry-ingest/scripts/report-active-installs.mjs#L61-L97","documentation":"dataRows extracts the row array from a Cloudflare SQL API response: either the payload itself is an array, or payload.data must be one. If the shape does not match — an error object, a string, or null came back — it throws 'Cloudflare SQL response did not contain a data array'. rowsFromResponse and newestEventFromResponse both depend on this extraction.","triggerScenarios":"The SQL endpoint returning HTTP 200 with a body lacking a data array (error/status object, wrapped payload, changed response schema); passing a parsed body from a different endpoint version.","commonSituations":"Cloudflare API evolution changing the envelope; a proxied or transformed response; empty responses on brand-new accounts with no telemetry rows.","solutions":["Log the raw payload before parsing to see the actual shape that arrived","Re-issue the query via the Cloudflare dashboard/API and compare response shapes","If the schema changed, update dataRows to read the new field while keeping the Array.isArray guard"],"exampleFix":"// before\nconst rows = payload?.data;\n// after\nconst rows = Array.isArray(payload) ? payload : payload?.data;\nif (!Array.isArray(rows)) {\n  throw new Error('Cloudflare SQL response did not contain a data array');\n}","handlingStrategy":"type-guard","validationCode":"if (!isSqlRowsPayload(payload)) {\n  console.error('unexpected Cloudflare SQL payload shape:', JSON.stringify(payload).slice(0, 200));\n  process.exit(2);\n}","typeGuard":"function isSqlRowsPayload(payload) {\n  return Array.isArray(payload) || Array.isArray(payload?.data);\n}","tryCatchPattern":"try {\n  rowsFromResponse(payload);\n} catch (error) {\n  if (/did not contain a data array/.test(error.message)) {\n    console.error('Cloudflare SQL payload changed — inspect raw response');\n    process.exit(2);\n  }\n  throw error;\n}","preventionTips":["Log the raw payload shape when integrating so schema drift is visible","Keep extraction behind one Array.isArray-guarded helper instead of ad-hoc field access"],"tags":["api","data-shape","parsing","cloudflare","type-guard"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}