Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS console log support #15402

Merged
merged 22 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ec1e145
Formatting for log lines.
mike12345567 Jan 15, 2025
6f9f36f
Getting string templates ready.
mike12345567 Jan 15, 2025
28958f5
Expose the ability to get logs.
mike12345567 Jan 16, 2025
d3a2306
Finishing link up of logs.
mike12345567 Jan 16, 2025
7efe88d
Merge branch 'binding-ts-improvements' of github.com:Budibase/budibas…
mike12345567 Jan 16, 2025
e6d536b
Getting the line number calculated correctly, as well as adding some …
mike12345567 Jan 16, 2025
e146d99
Adding in support for multi-parameter logs and actual logging to cons…
mike12345567 Jan 17, 2025
272bbf5
Logging with types - allows for coloured outputs.
mike12345567 Jan 17, 2025
bd5e554
Adding more test cases.
mike12345567 Jan 20, 2025
3b03515
Fixing test failure.
mike12345567 Jan 20, 2025
963b2e8
Merge branch 'master' into feature/js-logging
mike12345567 Jan 20, 2025
a920be3
Remove error.
mike12345567 Jan 20, 2025
fb41c72
Merge branch 'feature/js-logging' of github.com:Budibase/budibase int…
mike12345567 Jan 20, 2025
ae73c01
Adding test checks.
mike12345567 Jan 20, 2025
9c65f1a
Another quick fix.
mike12345567 Jan 20, 2025
98bd824
Adding the ability to configure whether or not string templates is te…
mike12345567 Jan 20, 2025
68374bc
Testing backend JS further.
mike12345567 Jan 20, 2025
04a7878
Changing how we enforce backend JS.
mike12345567 Jan 20, 2025
d51491a
Linting.
mike12345567 Jan 20, 2025
c5e4edc
Setting overflow-y in evaluation panel to auto.
mike12345567 Jan 21, 2025
866677a
Merge branch 'master' into feature/js-logging
mike12345567 Jan 21, 2025
dd5e920
Merge branch 'master' into feature/js-logging
mike12345567 Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/bbui/src/bbui.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
--purple: #806fde;
--purple-dark: #130080;

--error-bg: rgba(226, 109, 105, 0.3);
mike12345567 marked this conversation as resolved.
Show resolved Hide resolved
--warning-bg: rgba(255, 210, 106, 0.3);
--error-content: rgba(226, 109, 105, 0.6);
--warning-content: rgba(255, 210, 106, 0.6);

--rounded-small: 4px;
--rounded-medium: 8px;
--rounded-large: 16px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
decodeJSBinding,
encodeJSBinding,
processObjectSync,
processStringSync,
processStringWithLogsSync,
} from "@budibase/string-templates"
import { readableToRuntimeBinding } from "@/dataBinding"
import CodeEditor from "../CodeEditor/CodeEditor.svelte"
Expand Down Expand Up @@ -41,6 +41,7 @@
InsertAtPositionFn,
JSONValue,
} from "@budibase/types"
import type { Log } from "@budibase/string-templates"
import type { CompletionContext } from "@codemirror/autocomplete"

const dispatch = createEventDispatcher()
Expand All @@ -66,6 +67,7 @@
let insertAtPos: InsertAtPositionFn | undefined
let targetMode: BindingMode | null = null
let expressionResult: string | undefined
let expressionLogs: Log[] | undefined
let expressionError: string | undefined
let evaluating = false

Expand Down Expand Up @@ -157,7 +159,7 @@
(expression: string | null, context: any, snippets: Snippet[]) => {
try {
expressionError = undefined
expressionResult = processStringSync(
const output = processStringWithLogsSync(
expression || "",
{
...context,
Expand All @@ -167,6 +169,8 @@
noThrow: false,
}
)
expressionResult = output.result
expressionLogs = output.logs
} catch (err: any) {
expressionResult = undefined
expressionError = err
Expand Down Expand Up @@ -421,6 +425,7 @@
<EvaluationSidePanel
{expressionResult}
{expressionError}
{expressionLogs}
{evaluating}
expression={editorValue ? editorValue : ""}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@
import { Helpers } from "@budibase/bbui"
import { fade } from "svelte/transition"
import { UserScriptError } from "@budibase/string-templates"
import type { Log } from "@budibase/string-templates"
import type { JSONValue } from "@budibase/types"

// this can be essentially any primitive response from the JS function
export let expressionResult: JSONValue | undefined = undefined
export let expressionError: string | undefined = undefined
export let expressionLogs: Log[] = []
export let evaluating = false
export let expression: string | null = null

$: error = expressionError != null
$: empty = expression == null || expression?.trim() === ""
$: success = !error && !empty
$: highlightedResult = highlight(expressionResult)
$: highlightedLogs = expressionLogs.map(l => ({
log: highlight(l.log.join(", ")),
line: l.line,
type: l.type,
}))

const formatError = (err: any) => {
if (err.code === UserScriptError.code) {
Expand All @@ -25,14 +32,14 @@
}

// json can be any primitive type
const highlight = (json?: any | null) => {
const highlight = (json?: JSONValue | null) => {
if (json == null) {
return ""
}

// Attempt to parse and then stringify, in case this is valid result
try {
json = JSON.stringify(JSON.parse(json), null, 2)
json = JSON.stringify(JSON.parse(json as any), null, 2)
} catch (err) {
// couldn't parse/stringify, just treat it as the raw input
}
Expand Down Expand Up @@ -61,7 +68,7 @@
<div class="header" class:success class:error>
<div class="header-content">
{#if error}
<Icon name="Alert" color="var(--spectrum-global-color-red-600)" />
<Icon name="Alert" color="var(--error-content)" />
<div>Error</div>
{#if evaluating}
<div transition:fade|local={{ duration: 130 }}>
Expand Down Expand Up @@ -90,8 +97,36 @@
{:else if error}
{formatError(expressionError)}
{:else}
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
{@html highlightedResult}
<div class="output-lines">
{#each highlightedLogs as logLine}
<div
class="line"
class:error-log={logLine.type === "error"}
class:warn-log={logLine.type === "warn"}
>
<div class="icon-log">
{#if logLine.type === "error"}
<Icon
size="XS"
name="CloseCircle"
color="var(--error-content)"
/>
{:else if logLine.type === "warn"}
<Icon size="XS" name="Alert" color="var(--warning-content)" />
{/if}
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
<span>{@html logLine.log}</span>
</div>
{#if logLine.line}
<span style="color: var(--blue)">:{logLine.line}</span>
{/if}
</div>
{/each}
<div class="line">
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
{@html highlightedResult}
</div>
</div>
{/if}
</div>
</div>
Expand Down Expand Up @@ -130,10 +165,9 @@
height: 100%;
z-index: 1;
position: absolute;
opacity: 10%;
}
.header.error::before {
background: var(--spectrum-global-color-red-400);
background: var(--error-bg);
}
.body {
flex: 1 1 auto;
Expand All @@ -142,8 +176,26 @@
font-size: 12px;
overflow-y: scroll;
overflow-x: hidden;
white-space: pre-wrap;
white-space: pre-line;
word-wrap: break-word;
height: 0;
}
.output-lines {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.line {
border-bottom: var(--border-light);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: end;
padding: var(--spacing-s);
}
.icon-log {
display: flex;
gap: var(--spacing-s);
align-items: start;
}
</style>
5 changes: 5 additions & 0 deletions packages/server/src/jsRunner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import {
JsTimeoutError,
setJSRunner,
setOnErrorLog,
setTestingBackendJS,
} from "@budibase/string-templates"
import { context, logging } from "@budibase/backend-core"
import tracer from "dd-trace"
import { IsolatedVM } from "./vm"

export function init() {
// enforce that if we're using isolated-VM runner then we are running backend JS
if (env.isTest()) {
setTestingBackendJS()
}
setJSRunner((js: string, ctx: Record<string, any>) => {
return tracer.trace("runJS", {}, () => {
try {
Expand Down
23 changes: 23 additions & 0 deletions packages/string-templates/src/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function isJest() {
return (
process.env.NODE_ENV === "jest" ||
(process.env.JEST_WORKER_ID != null &&
process.env.JEST_WORKER_ID !== "null")
)
}

export function isTest() {
return isJest()
}

export const isJSAllowed = () => {
return process && !process.env.NO_JS
}

export const isTestingBackendJS = () => {
return process && process.env.BACKEND_JS
}

export const setTestingBackendJS = () => {
process.env.BACKEND_JS = "1"
}
57 changes: 52 additions & 5 deletions packages/string-templates/src/helpers/javascript.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { atob, isBackendService, isJSAllowed } from "../utilities"
import {
atob,
frontendWrapJS,
isBackendService,
isJSAllowed,
} from "../utilities"
import { LITERAL_MARKER } from "../helpers/constants"
import { getJsHelperList } from "./list"
import { iifeWrapper } from "../iife"
import { JsTimeoutError, UserScriptError } from "../errors"
import { cloneDeep } from "lodash/fp"
import { Log, LogType } from "../types"
import { isTest } from "../environment"

// The method of executing JS scripts depends on the bundle being built.
// This setter is used in the entrypoint (either index.js or index.mjs).
Expand Down Expand Up @@ -81,7 +88,7 @@ export function processJS(handlebars: string, context: any) {

let clonedContext: Record<string, any>
if (isBackendService()) {
// On the backned, values are copied across the isolated-vm boundary and
// On the backend, values are copied across the isolated-vm boundary and
// so we don't need to do any cloning here. This does create a fundamental
// difference in how JS executes on the frontend vs the backend, e.g.
// consider this snippet:
Expand All @@ -96,10 +103,9 @@ export function processJS(handlebars: string, context: any) {
clonedContext = cloneDeep(context)
}

const sandboxContext = {
const sandboxContext: Record<string, any> = {
$: (path: string) => getContextValue(path, clonedContext),
helpers: getJsHelperList(),

// Proxy to evaluate snippets when running in the browser
snippets: new Proxy(
{},
Expand All @@ -114,8 +120,49 @@ export function processJS(handlebars: string, context: any) {
),
}

const logs: Log[] = []
// logging only supported on frontend
if (!isBackendService()) {
// this counts the lines in the wrapped JS *before* the user's code, so that we can minus it
const jsLineCount = frontendWrapJS(js).split(js)[0].split("\n").length
const buildLogResponse = (type: LogType) => {
return (...props: any[]) => {
if (!isTest()) {
console[type](...props)
mike12345567 marked this conversation as resolved.
Show resolved Hide resolved
}
props.forEach((prop, index) => {
if (typeof prop === "object") {
props[index] = JSON.stringify(prop)
}
})
// quick way to find out what line this is being called from
// its an anonymous function and we look for the overall length to find the
// line number we care about (from the users function)
// JS stack traces are in the format function:line:column
const lineNumber = new Error().stack?.match(
/<anonymous>:(\d+):\d+/
mike12345567 marked this conversation as resolved.
Show resolved Hide resolved
)?.[1]
logs.push({
log: props,
line: lineNumber ? parseInt(lineNumber) - jsLineCount : undefined,
type,
})
}
}
sandboxContext.console = {
log: buildLogResponse("log"),
info: buildLogResponse("info"),
debug: buildLogResponse("debug"),
warn: buildLogResponse("warn"),
error: buildLogResponse("error"),
// table should be treated differently, but works the same
// as the rest of the logs for now
table: buildLogResponse("table"),
}
}

// Create a sandbox with our context and run the JS
const res = { data: runJS(js, sandboxContext) }
const res = { data: runJS(js, sandboxContext), logs }
return `{{${LITERAL_MARKER} js_result-${JSON.stringify(res)}}}`
} catch (error: any) {
onErrorLog && onErrorLog(error)
Expand Down
Loading
Loading