Files
dc/va-host
dc 90784b342f
Build / Build-and-ng-test (pull_request) Successful in 5m9s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m59s
Build / Build-and-test-development (pull_request) Successful in 33m54s
fix(va-host): deliver the load-mount message once, and document report.json from the file
Two review findings:

- The load handler re-posted pendingPayload on every load event of the frame,
  so an in-app navigation or a self-reload posted a filter that belonged to
  the mount which had already delivered it, and logged it as that mount's.
  The handler now takes the payload (clearing pendingPayload and
  pendingTiming) before posting, so a later load has nothing to send.

- The README illustrated report.json with one parameter while the committed
  file carries three, and the prose below reads as though one parameter were
  the shape. The example block is gone: the file is the example, and the keys
  are documented as a table with the parameter rules kept underneath.
2026-11-03 23:45:40 +00:00
..

VA report emulation (host for the embedded DC app)

A parent page that stands in for a SAS Visual Analytics report which embeds Data Controller as data-driven content. The dropdowns are the report's prompts; the iframe is framed the way VA frames the object, and is fed the same DDC message VA sends.

Two things this is not:

  • It is not part of the DC build. The host is not the child. Shipping it in DC's deployment would put a test harness on customer servers and let DC be opened top-level as a page that frames itself. Nothing here is in frontend.zip or sasjs_server.json.zip.
  • It is not a substitute for a real VA report. It reproduces the contract DC sees: the message shape, the delivery timing, the sandbox attribute set and the embed=va / va_parmap URL contract. It does not reproduce VA's UI or how VA computes a prompt's value.

Files

  • index.html - the report page: prompt bar, delivery/timing controls, the iframe, and a log.
  • host.js - reads the config, builds the message, mounts the frame, replays the message early, posts prompt changes.
  • report.json - the report definition (below). The committed copy points at a local server for local runs; deploy.mjs rewrites it for the target it deploys to.
  • deploy.mjs - deploys the three files above to a SASjs Server as an AppStream app.

report.json

The committed report.json is the working example, and it is the one to read: three parameters, because those are the three shapes that reach DC differently - a plain string, a month-name date string and a number.

key meaning
title heading on the page
server the server the app is served from; the frame's origin comes from it
app the AppStream name of the DC deployment, not its app-loc: where one folder has several names (DataController, DataController_Main, ...) any of them serves the same build, and this is the one the URL needs
table goes into the editor route: #/editor/<table>
sandbox the iframe's sandbox attribute set
mode refresh (reload per apply) or prompt (post into the loaded frame)
timing both, early or load
intervalMs, replayTimeoutMs how often the early replay posts, and for how long
parameters[] name, label, dataType, column, options, default
  • parameters[].label is the mapping key. DC matches va_parmap entries against the parameter label (not name, which is the report's own bookkeeping), case-insensitively, splitting each entry at its last -. column here becomes va_parmap=<label>-<column>; a parameter with no column is sent to the app but not mapped.
  • dataType is string or number and is preserved in the message: a numeric prompt and a month-name date string take different paths in DC's clause builder.
  • mode is refresh (reload the frame per apply - the report-refresh path) or prompt (post into the loaded frame - an in-place prompt change). timing is both (default), early or load: early posts only before the frame loads, load only after it has, and both does each, which is what VA does for a report with a stored prompt.

What the app receives

The message is the DDC shape VA sends, one entry per prompt, and dataType is honoured rather than cosmetic: a prompt configured as number arrives as 42, not "42", because DC's clause builder branches on the type and a numeric column compared with a quoted value is the failure this pair exists to catch. A prompt whose configured value is not numeric is left as it is rather than becoming NaN.

Deploying

# local, desktop-mode server (no login)
node deploy.mjs --server http://localhost:5000 --app clickme --table DC_JSLIB.MPE_X_TEST

# a server-mode estate (local login; credentials from SASJS_USER/SASJS_PASS)
SASJS_USER=me SASJS_PASS=... node deploy.mjs --server https://example.com \
  --app DataControllerDev --table DC996664.MPE_X_TEST --name va-host \
  --apploc /Public/app/va-host

# the same, with the password read from stdin so it stays out of argv and history
printf '%s' "$PASS" | node deploy.mjs --pass-stdin --server https://example.com ...

# re-check what is deployed, without deploying anything
node deploy.mjs --verify-only --server https://example.com --app DataControllerDev

There is no --pass: a password on the command line is readable from ps by every account on the host and is kept in the shell history. Use SASJS_PASS, or --pass-stdin to pipe one line in.

The script is the same route sasjs cbd uses: POST /SASjsApi/drive/deploy with a file tree plus streamWebFolder: "web" and streamServiceName, after which the server serves the folder at /AppStream/<name>/. No services are compiled, because the app is static.

The deploy is verified after it is sent. The route answering 2xx means the tree was accepted, not that the app is being served: a type: 'file' member that landed truncated still serves a 200 page, and an asset left over from a previous deploy is a 200 by definition. So the script fetches /AppStream/<name>/ and each of the three files back and compares them to the bytes it deployed, failing on any difference and naming the size and hash of both copies. A 401 from the page is reported as deployed-but-gated rather than as a failure, because on a server-mode estate that is the gate doing its job.

Two things worth knowing about that route:

  • type: 'file' members are written base64-decoded; type: 'service' members are plain text. A static app is therefore sent encoded (deploy.mjs does this). Sending plain text writes a truncated, unreadable file rather than failing.
  • A redeploy does not remove files the previous version had. The folder is written over. Deleting <appLoc> first, or deploying to a fresh app-loc, is the way to avoid carrying a stale asset.

The AppStream gate is authenticateAccessToken plus a per-app grant on /AppStream/<name>, so on a server-mode estate the page answers 401 until the user is signed in - which is also what makes the embedded app work, since both are served from the same origin and the session cookie is same-site.

The other harness, and where the contract lives

There are two things in this repo that stand in for a VA report, and they are not interchangeable:

  • client/cypress/e2e/va-ddc-host.cy.ts drives sasjs/server's /ddc-host page with a single message at timing: 'early', and asserts through the filter the mock backend stored. It is a fixture that runs in CI: no UI, one parameter, one delivery mode.
  • va-host/ is this page: a prompt bar, mode refresh/prompt, timing early/load/both, several parameters and a log, for a person driving the embed by hand.

Neither is the contract of record. The contract is what DC reads and when: the message shape and the window.__vaLastMessage buffer in client/src/app/services/va-messaging.service.ts, and the rule that a message must be captured before the app bootstraps, which is client/src/assets/va-early.js. The URL shape both harnesses build (/AppStream/<app>/#/editor/<table>?embed=va) comes from the app's own routing and parseEmbedParam.

So a change to any of those three - the message shape, the capture timing, the embed parameters - has to be read against both harnesses; a change to one harness's plumbing is local to it. In particular, host.js reads FRAME.contentWindow.__vaLastMessage to tell whether the message was captured, and Cypress deliberately does not (it proxies the framed document, so contentWindow is not trustworthy there). That difference is the environments', not a defect in either file.

Why same origin

On the deployments this emulates, the VA report and the DC app share a domain. That is the topology to reproduce: a host on a different origin would be blocked by the server's X-Frame-Options: SAMEORIGIN and frame-ancestors 'self', and the SameSite=Strict session cookie would not travel into the frame. DC's own trust rules agree - VaMessagingService.isTrustedEarlyOrigin accepts the embedding window's own origin, and the live path accepts a message whose event.source is the parent frame.

What to look for in the log

  • early replay: the app captured the message after N post(s) before frame loaded - the pre-bootstrap path, which is the one a report refresh exercises and the one that a single post on load cannot reach.
  • posted on load (timing=both) after frame loaded - the second copy VA sends, read on the app's ordinary path.
  • With timing=load: no replay lines at all, then frame loaded followed by posted on load (timing=load).
  • With mode=prompt: posted one message: and no mounting line - the metadata path, for an in-place prompt change.
  • If the replay gives up and also says the app does not expose window.__vaLastMessage, the probe has changed rather than the delivery: that global is how the replay knows to stop early.

In the embedded app, the FILTER bar shows the clause the report produced, e.g.

FILTER : (( SOME_DROPDOWN = 'Option 2' AND SOME_DATE = '12FEB1960'd AND SOME_NUM = 42 ))

A date arriving as a character value instead of a date literal, or a number arriving quoted, is the defect this pair exists to catch.

Nothing here reaches a DC release artefact

frontend.zip is built from client/dist and sasjs_server.json.zip from sasjs/sasjsbuild; va-host/ is outside both trees and is not referenced by sasjsconfig.json. Checked by building them: sasjs c -t server && sasjs b -t server produces a server.json file tree of 100 members (40 under /services/web) with no va-host entry, and client/dist holds 40 files, none of them va-host. The separation is a property of the build inputs, not a convention someone has to remember.