Closed lucasoares closed 5 months ago
Can you create a test that uses the run
as arrow function as below:
async run = () => {
await context.setVariableAsync('myVariable', 'myValue');
await context.deleteVariableAsync('myVariable');
const myVariable = await context.getVariableAsync('myVariable');
// myVariable: ''
}
Review your entire commit, considering the line size as 120 to make the line break.
I have a few points about the implementation of the extended functions that I would like discussed:
localhost
. In the current implementation of the HTTP function in ExecuteScritpV2
, how can we avoid this?ExecuteScritpV2
?There is a limit recursion and max statements?
Please check whether the use of "literal regular expression" is a limitation.
Can you check if this still a problem:
Javascript doesn't have the "Async" suffix as C# does as a common practice. Would be more "javascriptier" if we could use ".fetch", ".json", and so on.
Also would be great if we had extensions for the bucket, event-trackings and wrappers like context.getContact()
for the current contact, context.input
, and context.message
. @lucasoares are you accepting PRs for your PR?
I have a few points about the implementation of the extended functions that I would like discussed:
- Today we implemented some validations that prevent the user from using HTTP requests to access some internal servers, for example,
localhost
. In the current implementation of the HTTP function inExecuteScritpV2
, how can we avoid this?- The context is a complex database that we need to guarantee governance when accessing it, could you create a way to monitor this access when executed through
ExecuteScritpV2
?
It uses the same IHttpClient of the ProcessHttp action, which will apply all rules the same way
Nice question... All modifications to the variable is doing the same access the execute script already did in the SetScriptResultAsync
using the IContext
. I don't think there is too much difference right now, but I agree with you, we should have better observability when user is modifying the context variables. We can add trace information for context changes inside the execute script action in the future, this will improve how we monitor these changes. I don't think this PR must have a final version of the implementation, we will launch the execute script v2 as alpha for people to test, builder's team can improve it later as they wish. I think that ClearScript opens a lot of improvements we can do, adding more and more features making c# methods available inside the javascript to have better dev ex when using our systems.
Javascript doesn't have the "Async" suffix as C# does as a common practice. Would be more "javascriptier" if we could use ".fetch", ".json", and so on.
Also would be great if we had extensions for the bucket, event-trackings and wrappers like
context.getContact()
for the current contact,context.input
, andcontext.message
. @lucasoares are you accepting PRs for your PR?
I agree with you, adding more features to be accessed inside the Javascript will be great for the future, but I think we should create different PRs for the new features we may want to add, to start testing it with our customers. ClearScript opens a whole new possibilities for us to add features to the javascript. As @fadoaglauss said, we may want to create a pattern for being able to monitor all modifications/calls made by the users inside the javascript first. Feel free to open PRs and we may discuss these features and observability of them there!
About the async, it would be difficult to consider both patterns at the same time. If I want to remove the async suffix inside the javascript, I would need a C# method without the async suffix. I choose to keep c# patterns and then in the javascript will be clearer to the users they are using a async function just looking the name... I would like to keep this pattern to avoid creating c# smells.
There is a limit recursion and max statements?
No, there is not. But I personally think for now we don't need it. We should keep monitoring of course, and maybe lower the timeout or heap size limit.
The thing is, based on my benchmark, that Jint consumes too much memory when doing recursion or loops. For example executing this script:
function run() {
let a = 0;
while (a < 1000) {
a++;
}
return a;
}
Will consume 10MB memory on Jint (and 11.6s to execute) but only 54KB on ClearScript + V8 and will take almost the same time to execute as the simplest script used in the benchmark test.
We may use CPU usage to limit our user's scripts in the future, but I don't really know how we would do that. You may want to check this issue: https://github.com/microsoft/ClearScript/issues/578
I just added a recursion benchmark to show how bad our 4-years old outdated Jint deals with it:
1000 calls | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|---|
ExecuteScriptV2RecursionLoopScript | 1.605 ms | 0.0264 ms | 0.0247 ms | 3.9063 | 1.9531 | 56.84 KB | |
ExecuteScriptV1RecursionLoopScript | 32.415 ms | 0.2150 ms | 0.1795 ms | 1812.5000 | 437.5000 | 22328.14 KB |
10000 calls | Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|---|
ExecuteScriptV1RecursionLoopScript | NA | NA | NA | NA | NA | |
ExecuteScriptV2RecursionLoopScript | 2.396 ms | 0.0474 ms | 0.0695 ms | 3.9063 | 57.39 KB |
Jint just throws exception, even setting
100000000
as the recursion limit.
About the async, it would be difficult to consider both patterns at the same time. If I want to remove the async suffix inside the javascript, I would need a C# method without the async suffix. I choose to keep c# patterns and then in the javascript will be clearer to the users they are using a async function just looking the name... I would like to keep this pattern to avoid creating c# smells.
Actually you don't need to change the method names, you can just change your attribute loader "LowerCaseMembersLoader". You can add a check if the method returns a Task, remove the "Async" suffix.
But it's okay if your decision is to keep it anyway...
Actually you don't need to change the method names, you can just change your attribute loader "LowerCaseMembersLoader". You can add a check if the method returns a Task, remove the "Async" suffix.
But it's okay if your decision is to keep it anyway...
True, I could do it in the LowerCaseMembersLoader
loader. But then it would raise another issue when a class have both methods, with and without async suffix... I think we should let it as it is. Thanks for your input tho!
Using ClearScript V8 to execute JavaScript as a new action.
It doesn't changes the behavior of the current ExecuteScriptAction.
The new implementation allow users to use recent ECMAScript definitions with way less restrictions.
Additionally, users can execute HTTP request inside the JavaScript with a custom
request.fetchAsync
API.The new action also deals with user returns and parse it manually to store in the variable, awaiting any promises used by the JS script.
Code coverage: 87%:
Benchmark:
Conclusions I made from the benchmark:
Execution Time
: V1 is consistently faster than V2 depending on the complexity of the JS script. V2 seems to have a constant overhead regardless of script complexity, which can be good for us to have our system stable regardless of what our users create.Memory Allocation
: V2 allocates less memory (Gen0, Gen1, Allocated) compared to V1 and seems to have better memory management. V1 only allocated less memory executing the simplest script, but allocated 10MB of memory for a 1000-interactions loop doing absolutelly nothing, which can be the reason we had to set the MaxStatements in the past. I don't know if the memory allocated by V8 itself will be analyzed by the benchmarks, I will validate it.In summary I think V2 is more stable than V1 but it always take some time to configure and execute. In the future we may need to investigate what in the ExecuteScriptV2Action is spending more time.
Removing the RegisterFunctions and the result ScriptObjectConverter just to make sure it is not affecting the results:
For now I think the memory optimizations may have a good impact in our systems, even taking 1~1.5ms more to execute scripts.
Internal Exceptions
The new version have a new setting to capture all exceptions when executing the script.
When capturing exceptions, you may store the exception message in a variable to handle it in the flow.
Our internal exceptions are also captured but the message may be redacted for security reasons. A trace id will be provided in these cases and may be used to get more information about the exception logged by the system.
We also added a warning to the trace execution with the exception message, so you can see the error in the trace (in the Beholder extension when using Blip's Builder, for example).
If the variable name to store exception is not provided, the exception will be captured, the trace will have a warning message and the script will continue to execute normally without setting the variable.
Features and Limitations
The following documents all the functions available to use inside the JS:
Time Manipulation
Although the new implementation supports more recent time manipulation functions like
dateVariable.toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })
, we have some limitations with theExecuteScriptV2Action
.Limitations:
ExecuteScriptAction
, we do not set the local timezone for the script engine based on bot's configured timezone. We have modified three functions from date's prototype to use bot's timezone:Date.prototype.toDateString
,Date.prototype.toTimeString
andDate.prototype.toString
. It will use the bot's timezone when available (builder:#localTimeZone
in the flow configuration`). This is a limitation of the ClearScript + V8 engine.Other functions or even the native Date constructor to parse strings will not use bot's timezone if the date format doesn't includes the timezone in its format. It will use the local server engine timezone by default. We provide a parser helper to overcome that:
Example:
Date
object in the script result, it will be stored in the context variable with the following format:yyyy-MM-dd'T'HH:mm:ss.fffffffK
, which is the same of the default format used by thetime.dateToString
helper. If you need to parse the date in another script using the variable as input, you have two options:time.parseDate
documented below.time.dateToString
or native's date formats and then parse it in the other script you are using it as variable. Make sure to include the timezone in the string format if you choose a custom format.Date and time helpers:
ParseDate
time.parseDate(string date, object? options)
Function to parse a date string to a DateTime object.
It receives two parameters, with the last one being optional:
date
: The date string to be parsed.options
: optional parameter to configure options when parsing the date. Available options:format
: Optional parameter to set the format of the date string. If not provided it will try to infer the format, which may fail depending on the string.culture
: Optional parameter to infer the culture used in the string format. It will useen-US
if not set.timeZone
: Optional parameter to define which time zone should be used if the string doesn't includes the timezone in the format. If not set it will try to use bot's configured timezone or, if not available,America/Sao_Paulo
.time.parseDate
returns a JS Date object.Examples:
DateToString
time.dateToString(Date date, object? options)
Function to convert a Date object to a string using the bot's configured timezone or
America/Sao_Paulo
if not set.It receives two parameters:
date
: The Date object to be converted.options
: optional parameter to configure options when formatting the date. Available options:format
: Optional parameter to set the format of the date string. If not provided it will useyyyy-MM-dd'T'HH:mm:ss.fffffffK
.timeZone
: Optional parameter to define which time zone should be used to format the string. If not set it will try to use bot's configured timezone or, if not available,America/Sao_Paulo
.It returns a string.
Examples:
Sleep
Additionally, we have a helper function to sleep the script execution for a given amount of time:
time.sleep(int milliseconds)
It receives one parameter:
milliseconds
: The amount of time to sleep in milliseconds.Examples:
Context
We added some functions to interact with the context variables inside the script.
Set Variable
context.setVariableAsync(string name, object value, TimeSpan? expiration)
Async function to set variable to the context. Should be used in async context.
It receives three parameters, with the last one being optional:
name
: The name of the variable to be set.value
: The value to be set. Can be any object that will be serialized to use as the variable value.expiration
: Optional parameter to set the expiration time of the variable. If not set, the variable will not expire.Examples:
Get Variable
context.getVariableAsync(string name)
Async function to get variable from the context. Should be used in async context.
Returns an empty string if the variable does not exist.
It receives one parameter:
name
: The name of the variable to be retrieved.Examples:
Delete Variable
context.deleteVariableAsync(string name)
Async function to delete variable from the context. Should be used in async context.
It receives one parameter:
name
: The name of the variable to be deleted.Examples:
HTTP Requests
We added a new fetch API to allow users to make HTTP requests inside the script.
Fetch API
request.fetchAsync(string url, object? options)
An Async function to make HTTP requests inside the script. Should be used in async context.
It receives two parameters, with the last one being optional:
url
: The URL to make the request.options
: Optional parameter to set the request options. It must be a dictionary with the following optional properties:method
: The HTTP method to be used. Default: 'GET'.headers
: The headers to be sent with the request. Default: {}. Header values in the request options can be a string or an array of strings.body
: The body to be sent with the request. Default: null.It returns a object with the following properties:
status
: The status code of the response.headers
: The headers of the response. It is a dictionary with the header names as keys and the header values as values, where the values are arrays of strings.body
: The body of the response in string format.success
: A boolean indicating if the request was successful (200-299 status code range).The response also have the
jsonAsync()
method to parse the body as JSON.Examples: