Five walls that will bite anyone shipping on this stack
For each: the wall, the evidence, the workaround. Every citation traces back to a file in the repo.
Web Workers via Blob URLs are permanently blocked by Lightning Web Security
Any LWC on Tableau Next / Salesforce Lightning that uses Web Workers via Blob URLs is permanently blocked by LWS. That includes mapbox-gl-js, maplibre-gl-js, deck.gl — every WebGL library that spawns off-main-thread workers via new Worker(URL.createObjectURL(blob)).
Both Mapbox and MapLibre UMDs build their WebWorker as new Blob([workerBundleString], { type: 'text/javascript' }). Under LWS this throws immediately:
Lightning Web Security: Unsupported MIME type.Because the throw happens INSIDE factory(), the entire UMD wrapper unwinds — global.mapboxgl = factory() never assigns. Swap the MIME to application/javascript and the factory completes, but new mapboxgl.Map(...) then throws TypeError: Cannot read properties of undefined (reading 'protocol') inside the URL parser. The workaround is worse than the wall.
Rebuild as pure D3 Canvas/SVG with pre-baked GeoJSON, or use Leaflet with raster-only tiles. Leaflet does NOT use Blob workers. Avoid Leaflet.VectorGrid and vector tile plugins. Eight Mapbox LWCs (Hero, 3DExtrude, Heatmap, Traffic, Isochrone, HexAgg, TileMap, TerritoryMap) were rewritten to pure D3 in Wave S-map-D3. All 8 now render clean.
The MIME-swap "fix" makes it worse — surface an unrecoverable modal
The instinct when you see Lightning Web Security: Unsupported MIME type is to patch the library's Blob type to application/javascript. Do NOT do this. Making the factory succeed only relocates the failure to a louder place.
After patching, window.mapboxgl surfaces but every retry from the LWC's renderedCallback triggers:
WARNING: Too many active WebGL contexts. Oldest context will be lost. TypeError: Cannot read properties of undefined (reading 'protocol')
…and eventually surfaces Tableau's "Sorry to Interrupt" modal — worse than silent Mode-B black canvas. Data-URL fallback (data:application/javascript;base64,…) for the worker doesn't help either.
The silent Mode-B black canvas IS the preferred failure state. LWCs early-exit at if (!window.mapboxgl) return, the card chrome renders clean, and no user-visible error modal appears. The paved path is architectural: D3, Leaflet raster, or an iframe/LWR site outside the LWS boundary.
Data Cloud CSV ingestion is fully programmatic — the blocker was the OAuth scope
Every prior wave of this doc said CSV ingest to a DLO was blocked — Bulk API 2.0 rejects DLOs as "no create access," the streaming CSV endpoint is 404, and the Ingestion API path was called UI-only. That was wrong. The blocker was the OAuth scope on the token, not the endpoints.
Standard sf org login web yields a token with api refresh_token only. Every 400 against the CDP tenant endpoint was the tenant refusing an under-scoped token. Live-tested 4-stage pipeline works end-to-end once the token carries cdp_ingest_api:
1. POST /services/data/v67.0/ssot/connections → 201 {id, tenantSpecificEndpoint}
2. PUT /services/data/v67.0/ssot/connections/{id}/schema → 200
3. POST /services/data/v67.0/ssot/data-streams → 201 (DLO auto-materializes)
4. POST https://{tenant}.c360a.salesforce.com/api/v1/ingest/jobs → 201
→ PUT batches (text/csv, 10k/chunk) → PATCH state=UploadComplete → pollSingle-flag change to cold-open:
sf org login web \
--scopes "api cdp_ingest_api cdp_query_api refresh_token"No new Connected App. No Metadata deploy. Attendee sees the same OAuth consent popup with two extra scope lines. Definitive recipe at docs/dc-ingestion-research-2026-07-13.md.
Dashboard extension LWCs fetch /services/data and get 401 — the origin mismatch trap
Every canvas we mined assumes an LWC can call fetch("/services/data/v67.0/semantic-engine/gateway", { credentials: "include" }) directly from the browser. That assumption is wrong for the specific runtime an analytics__Dashboard LWC lives in.
An LWC hosted on analytics__Dashboard runs inside a document whose origin is <org>.lightning.force.com. Session cookies for /services/data/... live on <org>.my.salesforce.com. Browser-side fetch resolves against the lightning origin — no session cookie — and comes back:
HTTP 401 UnauthorizedEvery canvas author "deployed but never API-PATCHed onto a dashboard." That combination is what surfaces the mismatch.
Route every semantic-engine call through an Apex @AuraEnabled controller. All 45 gateway-using workshop LWCs have been refactored to Apex. Full reference at docs/references/official-dashboard-extension-sdk-docs.md.
Extension LWCs need an inner try/catch or the whole tile renders "Something Went Wrong"
Four of five Tier-1 offline map LWCs rendered "Something Went Wrong" while a fifth (ArcMap) rendered correctly — same fallback pattern, only ArcMap survived. Diagnosed 2026-07-11 on ck_map_showcase_dsh.
All 5 share an async _runQueryPipeline() wrapped in a single outer try/catch. When fetchDataUsingQueryAndSource() throws — because the SDM doesn't ship the field being queried (State_clc, Postal_Code on Account) — the outer catch sets _phase = "error" and the tile renders the framework's error state. ArcMap wraps the query call in an inner try/catch that swallows the error, sets rowArr = [], and falls through to _phase = "ready" → seed data.
// WRONG — an SDM-mismatch throw kills the tile const rows = await this._runShapeBQuery(fields, modelJson, sdmName, this._rowLimit); rowArr = Array.isArray(rows) ? rows : []; // RIGHT — seed-data path stays reachable try { const rows = await this._runShapeBQuery(fields, modelJson, sdmName, this._rowLimit); rowArr = Array.isArray(rows) ? rows : []; } catch (e) { if (this._debug) console.warn(TAG, "Shape B query failed; using seed data", e); rowArr = []; }
Corollary: the outer catch must call notifyLifecycleChange(LOADED) — never ERROR — so the dashboard shell keeps the tile visible.
18 more gotchas we hit and worked around
Compact table pulled from .claude/skills/tableau-next-author/references/platform-gotchas.md. Every row corresponds to a rule enforced by the authoring skill or a workaround shipped in bootstrap/lib/dashboard_builder.py.
| Name | One-line description | Severity |
|---|---|---|
| POST-vs-GET divergence | v67 POST rejects widgetStyle.textColor, source.label, source.type on widgets that GET happily returns. Round-tripping fails until you strip. | WORKS-AROUND |
| Filter widget source block | Missing source: {id, name} → widget renders but does nothing; server sets status DataSourceError and redacts field names to ACCESS_DENIED_*. | WORKS-AROUND |
Radial ?minorVersion=6 | Radial family (Radial Bar, Nightingale, Radial Donut) is not a Vizql variant. Wrong minor version returns "min version is 67.06". Only Color encoding accepted. | WORKS-AROUND |
| Relative-date operators | CurrentDay rejected. Fiscal / Next* variants inconsistent — sometimes 201, sometimes 400. Fall back to LastNDays. | WORKS-AROUND |
Total_Sales_clc is Closed-Won-only | SDM expression gates on Is_Won_Opportunity_clc, making the measure NULL for every non-Closed-Won stage. Across-stage visuals render 1 populated cell of 6. | KNOWN |
| Palette on-write required | Empty style.encodings.colors = SFDC pink/magenta default. Every viz payload must ship an explicit Handoff4 palette or the dashboard looks off-brand. | WORKS-AROUND |
| Text widget = Quill delta | Text widgets MUST be added via update_dashboard, not add_widget_to_dashboard. & HTML-encodes to & in labels. | WORKS-AROUND |
Boolean @api default | LWC1503: @api foo = true fails compilation with "Boolean public property must default to false." Rename the semantic so the default is false. | WORKS-AROUND |
Grouped Table mergeRepeatedCells | Must be false. subtotals block rejected (use grandTotals). Measure keys forbidden in style.headers.fields. | WORKS-AROUND |
| Map lat/lon aggregation | Lat/Lon must be Continuous. Platform applies SUM by default → summed lat/lon → world-scale drift near the antimeridian. Set Median or explicit pixel size. | WORKS-AROUND |
| Waterfall / Nightingale flags | isRunning: true and isNightingale: true rejected in v67.06. Ship as stacked bar / Radial Bar until parser catches up. | KNOWN |
| Required-but-undocumented viz fields | marks.headers.stack, full style.marks.panes block, correct size.type per chart. Missing fields → empty shell. | WORKS-AROUND |
Percent not a valid number type | numberFormatInfo.type: "Percent" returns 400. Use Number with % suffix, or NumberCustom. | WORKS-AROUND |
Probability_clc aggregation bug | SDM ships aggregationType: "Sum" on a percentage. Scatter payloads joining SUM(x), SUM(y) collapse to empty. Fix in SDM, not payload. | KNOWN |
| UserAgg CLC re-aggregation | CLC measures with aggregationType: "UserAgg" already contain SUM/COUNT inside the expression. Name-based auto-inference re-aggregates a pre-aggregated field. | WORKS-AROUND |
| Side-by-side bar MeasureNames | MeasureNames F2 must appear in BOTH columns AND style.headers.fields. Omit either → second measure column silently disappears. | WORKS-AROUND |
| DMO ingestion binding still UI-only | A freshly POSTed DMO comes back with isEnabled: false; SDM POST rejects it with SEMANTIC_FIELD_NOT_VALID. DMO enablement is a UI click. Bind SDMs to shipped ssot__ DMOs, or use the DLO ingest path (Finding 03). | KNOWN |
| SDM field discovery before map LWC | Workshop SDM has geo only on CloudKicks_Locations. Account_Latitude_clc, State_clc, Primary_Industry_clc do NOT exist. Always run SDM discovery before authoring. | WORKS-AROUND |
Eight paths tested for Data Cloud CSV ingestion
On 2026-07-13 we enumerated every documented and undocumented path to load CSV rows into a DLO from headless code. Seven paths were dead ends. One won. Path G was the answer the entire time — it was just gated behind a scope on the OAuth token.
By the numbers
platform-gotchas.md.Read the source. Fork the workarounds.
This isn't a whitepaper. Every finding above resolves to a file you can open, a payload you can POST, and a live org where we tested it.
orgfarm-bd675751b0.my.salesforce.com) on Tableau Next v67.06. Every payload was live-POSTed; every 400 response was captured; every workaround was diff-tested against a known-good baseline. The moat isn't the vision — it's the receipts.