Debugging Shopify Functions

How to use the Function Runner and Debug WASM and Rust

Stephen Biston
8 min readFeb 21, 2023

The Brave New World

Shopify Scripts are going away. Shopify Scripts are currently the preeminent way of quickly and easily implementing custom checkout, discount, and shipping experiences for Shopify Plus merchants. However, Shopify has announced that Scripts are being deprecated in favor of Shopify Functions as of August 2024. For developers, and the agencies and merchants that depend on them, this means two things:

  1. Customizing checkout/discounts/shipping is going to get significantly more complicated.
  2. Shopify developers must become familiar with working with Shopify Functions.

This post is under point #2, specifically covering working with Shopify Functions by learning how to debug them during development.

If you’re interested in point #1 and learning the basics about creating a Shopify Function, check out my guide to Shopify Functions wherein you create a simple discount function and learn how to configure it without an interface.

Using the Function Runner

Shopify has provided a great tool for emulating Shopify Function calls locally in their Function Runner tool, see the Github repo for it here. This tool is a small tool into which you can pipe function JSON input and receive the Function result, also JSON output. If you used the Shopify CLI app extension generation command to generate the Function then the Function Runner should already be installed in your project. If you want to install it manually, it’s as simple as running the following within your project:

npm install function-runner

You can add a -g flag to the above command to install globally instead.

Once you have the Function Runner installed, you can test your function by piping JSON input into and specifying your function’s WASM file. For example:

echo '{}' | npx function-runner --function extensions/my-function-ext/target/wasm32-wasi/release/my-function-ext.wasm

When this is run, depending on the specifics of your function’s code and what input it requires, you can expect output like the following:

      Benchmark Results

Name: my-function-ext.wasm
Runtime: 1.375458ms
Linear Memory Usage: 1088KB
Size: 155KB

Logs


Output
{
"discountApplicationStrategy": "FIRST",
"discounts": []
}

Optionally, you can pass the --json flag to the function-runner to get a purely JSON response — this can be useful for automating checks against the output. For example, a suite of tests can be created with a set of known inputs and expected outputs that can be run to guarantee behavior. Keep in mind that when passing arguments to a tool being invoked via npx you must pass it after a set of hyphen characters (--) like so:

echo '{}' | npx function-runner --function function.wasm -- --json

With the Function Runner, you can quickly test the expected output of a function without having to constantly re-deploy the function to the actual Shopify Infrastructure and recreate scenarios manually using a real user session. Instead, you can create the scenario once and copy the input it generates to the Function Extensions summary in your Partners Dashboard to test the scenario repeatedly with ease. Remember that one of the best debugging methods is to avoid needing to do it in the first place by testing thoroughly (with automation).

Interactive Debugging

While function-runner is great for holistic testing as a black-box where you submit input and know the expected output, sometimes you run into issues where you’re getting runtime errors emitted from your Function and you don’t know why. Sometimes, it will be sufficient to copy the input that generates the error from your app dashboard in the Partners Portal, but other times even if you’re able to reproduce the behavior you may not be able to understand it without a closer look. Interactive debugging is an invaluable tool in the developer’s toolbox for getting that closer look.

Debugging WASM

While you may be building your Shopify Functions in Rust or Typescript, it’s important to keep in mind that the actual running code for your Function is WebAssembly, or WASM. That means that, unless you’re writing the WASM code directly for your Function, the errors you see will not map 1-to-1 to the source code you’ve written and you’ll need to understand how to debug WASM code to best recreate the conditions under which the error is generated.

There are a lot of methods for debugging WASM interactively, but here we will go over a portable command-line-accessible tool: lldb. While low-level, the lldb tool is included with many operating systems and should be available to you but if not you can install it from here: https://apt.llvm.org/#llvmsh.

Firstly, to use lldb productively, it’s best to build your function WASM with debugging symbols included. This process will vary depending on what your Function is built with but for a Rust function, for example, this can be done using a slightly modified cargo wasi build command, run within the app extension directory root:

RUSTFLAGS=-g cargo wasi build

When this completes you can expect to see a new set of build files under a debug/ directory as opposed to the release/ directory. With the debug build completed, run the following command (substituting the directory and filenames with those appropriate for your project):

 lldb -- wasmtime -g target/wasm32-wasi/debug/rust-function-demo-ext.wasm

This command invokes the lldb interface wherein you can set up the debugger process — see the lines prepended with (lldb) for the instructions you will input.

(lldb) target create "wasmtime"
Current executable set to 'wasmtime' (arm64).
(lldb) settings set -- target.run-args "-g" "target/wasm32-wasi/debug/rust-function-demo-ext.wasm"
(lldb) target create "wasmtime"
Current executable set to 'wasmtime' (arm64).
(lldb) settings set -- target.run-args "-g" "target/wasm32-wasi/debug/rust-function-demo-ext.wasm"
(lldb) settings set plugin.jit-loader.gdb.enable on
(lldb) b main.rs:40
Breakpoint 1: no locations (pending).
WARNING: Unable to resolve breakpoint to any actual locations.
(lldb) run
Process 33962 launched: '/Users/your-username/.wasmtime/bin/wasmtime' (arm64)
1 location added to breakpoint 1

Notice that the b instruction (which sets a breakpoint) refers to the .rs file compiled into the WASM we referenced, the debugging symbols included in the debug cargo build allowed for this. In the scenario above, I set the breakpoint to the first line of code in my Function’s entry point method. Note also that the debugger complains that the breakpoint was unable to be resolved to an actual location but this is only until we invoke the process with run, the last line indicates that the location was added to the breakpoint successfully.

At this point in the process, the WASM is executing but holding for input — similar to the function-runner the program expects JSON input in a specific structure appropriate to your Function’s GraphQL API signature. Similarly, the easiest way to get this JSON is to copy it out of your app extension page in the Partners Dashboard. You can paste it directly into the terminal with the running debugger at this point:

Process 33962 launched: '/Users/your-username/.wasmtime/bin/wasmtime' (arm64)
1 location added to breakpoint 1
{
"discountNode": {
"metafield": {
"value": "{\"discount\":{\"discountType\":\"percentage\",\"value\":\"12.0\"}}"
}
},
"cart": {
"lines": [
{
"merchandise": {
"__typename": "ProductVariant",
"id": "gid://shopify/ProductVariant/43836028453104"
}
}
]
}
}Process 33987 stopped
* thread #1, name = 'main', queue = 'com.apple.main-thread', stop reason = breakpoint 1.3
frame #0: 0x0000000103939e14 JIT(0x1044bc000)`rust_function_demo_ext::function::h70e66810d2cb212b(input=ResponseData @ 0x00000003000ffcd0) at main.rs:40:9
37 #[shopify_function]
38 fn function(input: input::ResponseData) -> Result<output::FunctionResult> {
39 // Prep a response that indicates no discounts applied
-> 40 let no_discount = output::FunctionResult {
41 discounts: vec![],
42 discount_application_strategy: output::DiscountApplicationStrategy::FIRST,
43 };
Target 1: (wasmtime) stopped.
(lldb)

Now with the breakpoint hit and the execution suspended, you can begin the actual process of stepping through and debugging the code. If you’re familiar with interactive debugging, this should be familiar but the lldb instructions may be a bit opaque, so I would refer to the documentation for more insight: https://lldb.llvm.org/use/tutorial.html#controlling-your-program. You can optionally enter the gui instruction for a more helpful interface with directions and menus.

Debugging Rust with VSCode

Functions may be written in languages other than Rust but the current impression from Shopify is that Rust is the preferred language to use, and existing Shopify devs likely have less exposure than they might to other options like Javascript and Typescript — that’s why only Rust is covered here.

If you’re thinking that all of the above with lldb is a bit cryptic and that there must be a better way, you’re half right. There are a few better ways, but they have their caveats — I suggest learning the lldb method at least in a basic sense to have that option open to you if the constraints of the friendlier methods make them inaccessible for your purposes.

A less direct method of testing that is more user-friendly is using the VSCode debugger with the lldb and rust-analyzer plugins to interactively debug Rust code directly. First, install those two plugins:

  1. VSCode LLDB
  2. Rust Analyzer

Once the plugins are installed, you need only add a simple configuration entry in your launch.json file, similar to the following:

{
"type": "lldb",
"request": "launch",
"name": "Run Shopify Function Tests",
"cargo": {
"args": [
"test",
"--manifest-path=${workspaceFolder}/extensions/rust-function-demo-ext/Cargo.toml"
]
},
"cwd": "${workspaceFolder}",
"args": []
}

The above will run the suite of tests in tests.rs associated with your Shopify Function—included by default if you generated your extension with the Shopify CLI. All you need to do following this is set a breakpoint in your test code or in the Function code proper depending on exactly what you want to test. Then just run the debug configuration:

VSCode Debugger running test input against main.rs

Now you can begin debugging! Step through the code using the pleasant VSCode interface and get to the root of your Function’s problems. This is often a good place to start when a Runtime Error presents itself in the Partners Dashboard; add the corresponding input into a test case within tests.rs and re-run the debugging configuration. Note if you want to only execute a single test you can duplicate the launch.json configuration we added but add an additional entry in args to specify the exact test case in the test file (something like test_mod_name::test_fn_name).

Final Thoughts and Further Reading

This is by no means a comprehensive guide on how one might debug WASM or Rust, and if you’re an experienced WASM or Rust dev you probably have workflows that work well for you already. However, WebAssembly and Rust are probably relatively foreign environments for the average Shopify developer, and the forthcoming shift from simple Shopify Scripts to these complex “Functions” could prove challenging. Despite that, if you’re able to set yourself up locally and debug the code you’re writing efficiently, you’re halfway to a proficient developer.

So, before the August 2024 due date be sure you’re ready to start working on migrating the more complex Shopify Scripts to custom apps using Shopify Functions—and every time you find yourself debugging a problem make sure you make a test case to prevent a regression!

For more information surrounding Shopify Function debugging, check out the following links:

--

--

Stephen Biston

Web Dev & Technical Lead with over a decade of experience. Did my content help you out? Feel free to buy me a coffee: https://ko-fi.com/stephenbiston