diff --git a/docs/dcc-tables.md b/docs/dcc-tables.md index f67679c..6107732 100644 --- a/docs/dcc-tables.md +++ b/docs/dcc-tables.md @@ -217,6 +217,8 @@ The code is simply `%include`'d at the relevant point during backend execution. If the entry ends in `".sas"` it is assumed to be a physical, filesystem file. Otherwise, the source code is extracted from SAS Drive or Metadata. +The hook shares the WORK library of the service that runs it, so it can read whatever that service received. In particular the two browser tables are there whenever the app made the request - `work.browser_info` (the page URL, the embedding document, timezone, locale, versions) and `work.browser_url_vars` (the page's URL parameters, one row each). See [Browser Variables](/developer-guide/browser-variables/). A `POST_EDIT_HOOK` that runs on an uploaded file is the exception: the upload path is multipart and carries no input tables. + To illustrate: * Physical filesystem (ends in .sas): `/opt/sas/code/myprogram.sas` diff --git a/docs/dci-troubleshooting.md b/docs/dci-troubleshooting.md index 026ab4c..66f2638 100644 --- a/docs/dci-troubleshooting.md +++ b/docs/dci-troubleshooting.md @@ -142,97 +142,24 @@ The app version is bundled into the frontend during the release, and is visible You can also determine the app version (and SASjs Version, and build time) by opening browser Development Tools and running `appinfo()` in the console. +## The app does not start - Show log + +A startup failure shows a modal with the error and the service that failed. When that service is `public/startupservice`, the modal offers **Show log**, which opens the service in a new browser window with `_debug=131`, so the SAS log is the page body. + +The button exists because of how Viya runs a request from JavaScript: it can go through a Compute execution task (`_executionTasks=true`), where the log is not returned and the request can time out before the service finishes. The browser request runs the same stored program without a task, so the log is available even when the adapter got nothing back. On SAS 9 it opens the stored process, and on SASjs Server the route returns the service output rather than a log. + ## Support Diagnostics -The frontend sends two input tables with the startup service and with the services that execute customer-provided code - the [hook scripts](macros.md) (pre/post edit and approve hooks) and the dynamic cell dropdown programs. They are available in those services as `work.browser_info` and `work.browser_url_vars`, so a support ticket can be diagnosed from the job log without asking the user follow-up questions, and a hook can adapt to its context. Other services do not receive them. - -The services that receive the tables are: `public/startupservice`, `editors/getdata`, `editors/getdynamiccolvals`, `editors/stagedata`, `editors/loadfile`, `editors/restore` and `auditors/postdata`. - -`editors/loadfile` is the exception in practice: the app reaches it through the adapter's file-upload call (multipart), which carries no input tables, so the service - and the post edit hook it runs - does not receive them in normal use. It is on the list for completeness, and would receive them if the service were ever invoked through the ordinary request path. +The frontend sends two input tables with the startup service and with the services that execute customer-provided code - the [hook scripts](dcc-tables.md#hook-scripts) (pre/post edit and approve hooks) and the dynamic cell dropdown programs. See [Browser Variables](/developer-guide/browser-variables/) for the column lists, the log lines, how to read them from a hook, and which services receive them. ### browser_info -A single-row table with these columns: - -| Column | Description | -|---|---| -| `url` | The URL of the Data Controller page itself (the iframe), not the document embedding it. When the editor is embedded in a report, the embedding URL is visible here. | -| `referrer` | The embedding document, from `document.referrer`. For an embedded report this is the report URL, so the embedding report can be told apart from the editor URL. | -| `timezone` | The browser's IANA timezone name, e.g. `Europe/Berlin`. | -| `tz_offset` | The browser's UTC offset in minutes, as `Date.getTimezoneOffset()` returns it (positive west of UTC, so `Europe/Berlin` in summer is -120). | -| `locale` | The browser locale, e.g. `en-GB`. | -| `dc_version` | The Data Controller build, as shown by `appinfo()`. | -| `adapter_version` | The `@sasjs/adapter` version the client was built with. | -| `browser` | The browser family, parsed from the user agent. | -| `browser_version` | The browser version. | -| `platform` | The operating system, parsed from the user agent. | -| `user_agent` | The raw `navigator.userAgent` string. | +The browser context: page URL, embedding document, timezone, locale, versions, and the user agent. Columns and examples: [Browser Variables](/developer-guide/browser-variables/#browser_info). ### browser_url_vars -The page URL parameters as one row per parameter, so a SAS program can read a parameter by name rather than parse the `url` string: - -| Column | Description | -|---|---| -| `name` | The URL parameter name. | -| `value` | The URL parameter value. | - -Parameters from both the search string and the hash query string (Angular routes carry them after the `#`) are included; on a name collision the search string value wins. The table is sent only when the page URL has at least one parameter. - -On SAS 9 and Viya the app is reached through the platform's own job URL, so the platform's parameters (`_FILE`, `_program`, `_debug`, `_webout` and friends) are in the table alongside DC's. Read a parameter by name rather than assuming every row belongs to Data Controller - `embed` and `labels` are DC's, `_FILE` and `_program` are the platform's. - -The values in both tables are supplied by the client, so treat them as diagnostics hints rather than a security boundary. - -To see the tables, run any of the services above with debug on. The debug value depends on the path: `&_debug=131` on the Compute API and SAS 9, or `&_debug=128` on the Viya web (JES) path with `runAsTask` enabled - which is what the frontend sends there, so turning debug on in the app is enough. Either value makes the session initialisation write the tables to the job log: - -``` -NOTE: MPEINIT: work.browser_url_vars: -name=embed value=va -NOTE: MPEINIT: work.browser_info: -url=... referrer=... timezone=Europe/Berlin tz_offset=-120 locale=en-GB -dc_version=7.15.0 adapter_version=4.19.0 browser=Chrome browser_version=120.0 -platform=Linux user_agent=... -``` - -A service called directly (for instance from a URL, or by a script), or by an older frontend, has no `work.browser_info` - the log then records that fact instead. +The page URL parameters, one row per parameter. Read a parameter by name rather than parsing the page URL - the platform's own parameters are in there too. Columns and examples: [Browser Variables](/developer-guide/browser-variables/#browser_url_vars). ### Reading the tables from a hook -A [hook script](macros.md) is `%include`d into the running service, so it shares the service's WORK library and can read the tables directly. Take an embedded editor opened at: - -``` -https://datacontroller.io/#/editor/SASHELP.CLASS?labels=true&embed=va -``` - -`browser_url_vars` then holds a row per parameter - `labels=true` and `embed=va` - so a hook can pick up either by name: - -```sas -%let labels=N; -%let embed=N; - -%if %sysfunc(exist(work.browser_url_vars)) %then %do; - proc sql noprint; - select value into :labels - from work.browser_url_vars - where name = 'labels'; - select value into :embed - from work.browser_url_vars - where name = 'embed'; - quit; -%end; -``` - -To branch on what the client reports: - -```sas -%if %sysfunc(exist(work.browser_info)) %then %do; - data _null_; - set work.browser_info; - call symputx('dc_timezone', timezone); - call symputx('dc_locale', locale); - run; -%end; -``` - -!!! warning - Guard with `%sysfunc(exist())` before reading. A service called directly, or by a frontend older than this feature, has no `work.browser_info` at all, so an unguarded `set` or `select` fails the job. \ No newline at end of file +Guard the read with `%sysfunc(exist())` - a service called directly has no `work.browser_info`. Worked examples: [Browser Variables](/developer-guide/browser-variables/#reading-them-from-a-hook). diff --git a/docs/derived-variables.md b/docs/derived-variables.md index 9d087e7..03f7cb0 100644 --- a/docs/derived-variables.md +++ b/docs/derived-variables.md @@ -120,6 +120,8 @@ Derived columns themselves are an EDIT screen feature: the VIEW screen and the d - **Dependencies declared.** `VAR_DEPS` lists every input the expression uses - physical columns and derived variables alike. Statements are generated in dependency order, so no sequence number is maintained, and the declaration is checked in both directions: every reference must be declared, and every declared name must be referenced and must exist. Circular references are rejected. - **No collisions.** `VAR_NM` must not match an existing column of the target table, or `_____DELETE__THIS__RECORD_____`. - **Read-only.** Derived columns cannot be edited in the grid, and any value submitted for them is discarded before the approval package is created. + - **Label is free text.** `VAR_LABEL` takes any characters, in any script - quotes are escaped when the statement is generated - but it is a column label, so it is one line: control characters and line breaks are refused, and so is a value over 256 characters, which the engine would otherwise cut down silently. + - **Not for row level security.** A derived variable cannot be named by an active [row level security](/row-level-security/) rule, of any scope. RLS rules are validated against the physical table, where a derived column does not exist, so a rule naming one is rejected when it is submitted. - **Recomputed on every read.** The value always reflects the current data and the current definition, and there is no history of previous derived values. A configuration error - a bad name, an invalid type or length, a name collision, or a reference to a derived variable that is not declared in `VAR_DEPS` - stops the EDIT screen with a message naming the variable. A syntax error or an unknown column in the expression itself is caught when the view is compiled, and stops the screen with a message naming the table. diff --git a/docs/developer-guide/browser-variables.md b/docs/developer-guide/browser-variables.md new file mode 100644 index 0000000..385ff78 --- /dev/null +++ b/docs/developer-guide/browser-variables.md @@ -0,0 +1,113 @@ +--- +layout: article +title: Browser Variables +description: The frontend sends the browser context to the backend as two input tables - browser_info and browser_url_vars - available to the startup service, the edit services and hook scripts. +--- + +# Browser Variables + +Two input tables travel with most requests, so a SAS program can see the browser it is serving rather than asking the user: + +| Table | Holds | +|---|---| +| `work.browser_info` | one row - the page URL, the document embedding it, the browser timezone and locale, and the versions in play | +| `work.browser_url_vars` | one row per parameter on the page URL | + +They exist for two jobs: diagnosing a support ticket from the job log, and letting a [hook script](/dcc-tables/#hook-scripts) adapt to how the app was opened - embedded in a VA report, opened with `?labels=true`, or used by someone in another timezone. + +## Which services receive them + +`public/startupservice`, `editors/getdata`, `editors/getdynamiccolvals`, `editors/stagedata`, `editors/loadfile`, `editors/restore` and `auditors/postdata`. Other services do not receive them. + +`editors/loadfile` is the exception in practice: the app reaches it through the adapter's multipart upload, which carries no input tables, so the service - and the post edit hook it runs - does not receive them in normal use. It is on the list for completeness, and would receive them if it were ever invoked through the ordinary request path. + +## browser_info + +A single row with these columns: + +| Column | Description | +|---|---| +| `url` | The URL of the Data Controller page itself (the iframe), not the document embedding it. When the editor is embedded in a report, the embedding URL is visible here. | +| `referrer` | The embedding document, from `document.referrer`. For an embedded report this is the report URL, so the embedding report can be told apart from the editor URL. | +| `timezone` | The browser's IANA timezone name, e.g. `Europe/Berlin`. | +| `tz_offset` | The browser's UTC offset in minutes, as `Date.getTimezoneOffset()` returns it (positive west of UTC, so `Europe/Berlin` in summer is -120). | +| `locale` | The browser locale, e.g. `en-GB`. | +| `dc_version` | The Data Controller build, as shown by `appinfo()`. | +| `adapter_version` | The `@sasjs/adapter` version the client was built with. | +| `browser` | The browser family, parsed from the user agent. | +| `browser_version` | The browser version. | +| `platform` | The operating system, parsed from the user agent. | +| `user_agent` | The raw `navigator.userAgent` string. | + +## browser_url_vars + +The page URL parameters, one row per parameter, so a SAS program can read a parameter by name rather than parse the `url` string: + +| Column | Description | +|---|---| +| `name` | The URL parameter name. | +| `value` | The URL parameter value. | + +Parameters from both the search string and the hash query string (Angular routes carry them after the `#`) are included; on a name collision the search string value wins. The table is sent only when the page URL has at least one parameter. + +On SAS 9 and Viya the app is reached through the platform's own job URL, so the platform's parameters (`_FILE`, `_program`, `_debug`, `_webout` and friends) are in the table alongside Data Controller's. Read a parameter by name rather than assuming every row belongs to the app - `embed` and `labels` are Data Controller's, `_FILE` and `_program` are the platform's. + +## Seeing them in the job log + +Run any of the services above with debug on. The value depends on the path: `&_debug=131` on the Compute API and SAS 9, or `&_debug=128` on the Viya web (JES) path with `runAsTask` enabled - which is what the frontend sends there, so turning debug on in the app is enough. Either value makes the session initialisation write both tables to the job log: + +``` +NOTE: MPEINIT: work.browser_url_vars: +name=embed value=va +NOTE: MPEINIT: work.browser_info: +url=... referrer=... timezone=Europe/Berlin tz_offset=-120 locale=en-GB +dc_version=7.15.0 adapter_version=4.19.0 browser=Chrome browser_version=120.0 +platform=Linux user_agent=... +``` + +A service called directly (for instance from a URL, or by a script), or by a frontend older than this feature, has no `work.browser_info` - the log then records that fact instead. + +## Reading them from a hook + +A [hook script](/dcc-tables/#hook-scripts) is `%include`d into the running service, so it shares the service's WORK library and can read the tables directly. Take an embedded editor opened at: + +``` +https://datacontroller.io/#/editor/SASHELP.CLASS?labels=true&embed=va +``` + +`browser_url_vars` then holds a row per parameter - `labels=true` and `embed=va` - so a hook can pick up either by name: + +```sas +%let labels=N; +%let embed=N; + +%if %sysfunc(exist(work.browser_url_vars)) %then %do; + proc sql noprint; + select value into :labels + from work.browser_url_vars + where name = 'labels'; + select value into :embed + from work.browser_url_vars + where name = 'embed'; + quit; +%end; +``` + +To branch on what the client reports: + +```sas +%if %sysfunc(exist(work.browser_info)) %then %do; + data _null_; + set work.browser_info; + call symputx('dc_timezone', timezone); + call symputx('dc_locale', locale); + run; +%end; +``` + +!!! warning + Guard with `%sysfunc(exist())` before reading. A service called directly, or a frontend older than this feature, has no `work.browser_info` at all, so an unguarded `set` or `select` fails the job. + +## What they are not + +Both tables are supplied by the client, so treat them as diagnostic hints rather than a security boundary. Never authorise on anything in them - a caller can send whatever it likes. diff --git a/docs/developer-guide/filter-pre-edit-hook.md b/docs/developer-guide/filter-pre-edit-hook.md index 2ee4a59..8bbc2b1 100644 --- a/docs/developer-guide/filter-pre-edit-hook.md +++ b/docs/developer-guide/filter-pre-edit-hook.md @@ -19,6 +19,8 @@ When a filtered editor loads, `editors/getdata` returns the filter to the client The response does not carry the filter key (`FILTER_RK`). That is an *input* to `getdata`, not an output - the editor already holds it in the URL (`/editor//`), so there is nothing to return. +The hook also receives the browser tables that came in with the request - `work.browser_info` and `work.browser_url_vars` - so it can tell an embedded editor from a standalone one, or take a value from the page URL. See [Browser Variables](/developer-guide/browser-variables/). + ## When the hook runs `getdata` builds the filter, applies it, and sorts the result, all before the pre-edit hook runs. In order: diff --git a/docs/developer-guide/two-rule-sets-one-table.md b/docs/developer-guide/two-rule-sets-one-table.md index e42ef8a..98d5341 100644 --- a/docs/developer-guide/two-rule-sets-one-table.md +++ b/docs/developer-guide/two-rule-sets-one-table.md @@ -100,6 +100,8 @@ run; From that moment the changeset is an approval against `ORDERS.ORDERS`. The approver sees a diff against the real table, the load writes to the real table, and the mirror is never touched. +While it runs, the hook can also read `work.browser_info` and `work.browser_url_vars` - the browser context that came with the submit - to tell an embedded editor from a standalone one, or to pick up a parameter from the page URL. See [Browser Variables](/developer-guide/browser-variables/). + ![A change submitted against the mirror, raised against the real table](/img/approval-routed.png) ### Reassigning LIBREF and DS diff --git a/docs/embed-va.md b/docs/embed-va.md index ab0cd4f..5615eea 100644 --- a/docs/embed-va.md +++ b/docs/embed-va.md @@ -53,6 +53,8 @@ In the VA Report Designer, add a **Data-Driven Content** object and set the URL ## Debugging +A hook script can tell an embedded editor from a standalone one without any client-side inspection: `work.browser_info` carries the page URL and `referrer`, and `work.browser_url_vars` carries `embed=va`. See [Browser Variables](/developer-guide/browser-variables/). + The following snippet can be used in console to dump the values being provided to DC from VA: ``` diff --git a/docs/row-level-security.md b/docs/row-level-security.md index 7c3ae88..2d71046 100644 --- a/docs/row-level-security.md +++ b/docs/row-level-security.md @@ -41,6 +41,8 @@ This variable is an integer, and used to divide a complex filter into numerous s ### RLS_VARIABLE_NM This is the name of the variable against which a filter value will be applied +It must be a physical column of the table. A [derived variable](/derived-variables/) cannot be named here: rules are validated against the table itself, where a derived column does not exist, so such a rule is rejected when it is submitted. + ### RLS_OPERATOR The available operator will depend on whether the column is character or numeric. Example values: diff --git a/mkdocs.yml b/mkdocs.yml index 0a00ee2..260c64b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,7 @@ nav: - Developer Guide: - Filter in a Pre-Edit Hook: developer-guide/filter-pre-edit-hook.md - One Table, Two Rule Sets: developer-guide/two-rule-sets-one-table.md + - Browser Variables: developer-guide/browser-variables.md - Table Guide: - MPE_AUDIT: tables/mpe_audit.md - MPE_COLUMN_LEVEL_SECURITY: tables/mpe_column_level_security.md