debug

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: MIT Imports: 13 Imported by: 0

README

xk6-debug

A k6 extension for debugging JavaScript test scripts. Inspect variables and pause at breakpoints while your k6 script runs — in the terminal or with full VS Code/Cursor IDE integration.

Getting Started

This walkthrough takes you from zero to a working debug session. You will:

  1. Build a custom k6 binary with the debug extension
  2. Write a simple k6 script with a debugger statement
  3. Preprocess the script (one command)
  4. Run it and see variable values printed to your terminal
Prerequisites
  • Go 1.25+
  • Node.js 18+
  • A local HTTP server on port 8000 (any will do — python3 -m http.server 8000 works)
Step 1: Build the custom k6 binary

From the xk6-debug/ directory:

go build -o k6-debug ./cmd/k6debug/

This produces a ./k6-debug binary with the debug extension built in.

Step 2: Write a test script

Create a file called script.js:

import http from 'k6/http';

export const options = {
  vus: 1,
  iterations: 1,
};

export default function () {
  let resp = http.get('http://localhost:8000');
  debugger;
  let status = resp.status;
  let body = resp.body;
}

The debugger statement is the key part. It tells the debugger to pause execution at that line, just like it would in a browser.

Step 3: Preprocess the script

The preprocessor rewrites your script to add debug hooks. Run it once before each debug session:

# Install dependencies (first time only)
cd preprocessor && npm install && cd ..

# Preprocess
./preprocessor/bin/k6-debug-preprocess script.js -o /tmp/debug_script.js

The preprocessor:

  • Injects import { capture, breakpoint, enterScope } from 'k6/x/debug'
  • Inserts capture() after each variable assignment to record values
  • Replaces debugger statements with breakpoint() calls
  • Injects enterScope() at the top of each function/block to track variable scoping
  • Tags each instrumented call with its lexical scope ID so the debugger only shows in-scope variables
Step 4: Start a local server

Open a separate terminal and start any HTTP server:

python3 -m http.server 8000
Step 5: Run the script
K6_AUTO_EXTENSION_RESOLUTION=false ./k6-debug run /tmp/debug_script.js
What you should see

After the HTTP request completes, two things happen:

Variable captures appear on stderr. Each captured variable prints as a JSON line:

{"type":"capture","vu":1,"file":"script.js","line":9,"col":6,"name":"resp","value":{ ... }}

You will see captures for resp, status, and body — showing their values at the moment they were assigned.

The script pauses at debugger. The terminal prints:

[k6-debug] Breakpoint hit at script.js:10 (VU #1). Press Enter to continue...

The VU is frozen. No more code runs until you press Enter. This is how you know the debugger is working — the script stops exactly where you placed debugger, and you can see the variables that were captured before that point.

Press Enter to resume. The remaining variables (status, body) are captured and the script finishes normally.


VS Code / Cursor IDE Integration (DAP mode)

For a full IDE debugging experience with breakpoints, stepping, and variable inspection. The extension handles instrumentation, launching, and attaching automatically.

Step 1: Install the VS Code extension
cd vscode/k6-debug-extension
npm install && npm run package

Then install the .vsix file:

  • VS Code: code --install-extension k6-debug-0.1.0.vsix
  • Cursor: cursor --install-extension k6-debug-0.1.0.vsix
Step 2: Configure launch.json

Add this to your .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug k6 Script",
      "type": "k6",
      "request": "launch",
      "program": "${file}"
    }
  ]
}

Tip: Open any k6 script and press F5 — VS Code will offer to create this configuration automatically.

Step 3: Press F5

Open your original k6 script (no preprocessing needed) and press F5. The extension will:

  1. Instrument the script automatically
  2. Find a free port
  3. Start k6-debug with the DAP server
  4. Attach the debugger once the server is ready

Set breakpoints by clicking in the gutter before or after pressing F5.

  • When a breakpoint is hit, the editor highlights the paused line
  • The Variables panel shows captured variables — only those in scope at the pause point
  • Variables appear in source order and stale values from previous iterations are automatically cleared
Configuration
Setting Description Default
k6debug.binaryPath Path to the k6-debug binary Auto-detect (workspace root, then PATH)

To set a custom binary path, add to your VS Code settings:

{
  "k6debug.binaryPath": "/path/to/k6-debug"
}

To pass extra k6 flags (e.g. change VU count):

{
  "name": "Debug k6 Script",
  "type": "k6",
  "request": "launch",
  "program": "${file}",
  "k6Args": ["--vus", "2", "--iterations", "5"]
}
Attach mode (advanced)

If you prefer to start k6 manually and attach separately:

K6_DEBUG_DAP=:4711 K6_AUTO_EXTENSION_RESOLUTION=false ./k6-debug run /tmp/debug_script.js
{
  "name": "Attach to k6 debugger",
  "type": "k6",
  "request": "attach",
  "port": 4711
}
Debugger controls
Button Action
Continue (F5) Resumes all VUs until the next breakpoint
Step Over (F10) Steps to the next line in the current VU; other VUs stay frozen
Disconnect / Stop Resumes all VUs, kills k6, and ends the debug session
Multi-VU debugging

When running with multiple VUs (vus: 2 or more), the debugger uses all-stop mode:

  • When one VU hits a breakpoint, all other VUs are frozen
  • Continue resumes all VUs
  • Step Over resumes only the active VU — others remain frozen until Continue
  • Each VU appears as a separate thread in the Call Stack panel
Troubleshooting
  • Nothing happens on F5: Check the "k6 Debug" output channel in VS Code for error details.
  • Binary not found: Set k6debug.binaryPath in VS Code settings to the full path of your k6-debug binary.
  • Port conflict: The extension picks a random free port automatically — no manual port management needed in launch mode.
  • Breakpoints not hitting: Ensure launch.json uses "request": "launch" (not "attach") so the extension instruments the script. Raw uninstrumented scripts won't trigger the debugger.
  • HTTP requests failing: Make sure any server your script needs is running before pressing F5.

How it works

The preprocessor transforms your script before k6 runs it:

Your code:

let resp = http.get('http://localhost:8000');
debugger;
let status = resp.status;

After preprocessing:

import { capture, breakpoint, enterScope } from 'k6/x/debug';

export default function() {
  enterScope(1, 0);
  let resp = http.get('http://localhost:8000');
  capture({ line: 9, col: 6, name: "resp", file: "script.js", scope: 1 }, resp);
  breakpoint({ line: 10, col: 2, file: "script.js", scope: 1 });
  let status = resp.status;
  capture({ line: 11, col: 6, name: "status", file: "script.js", scope: 1 }, status);
}
  • enterScope(id, parentId) registers a lexical scope and clears stale variables from previous entries
  • capture() records a variable's value right after assignment, tagged with its scope
  • breakpoint() replaces debugger and pauses execution
Variable scoping

The debugger tracks lexical scopes from the AST:

  • Global scope (0): top-level variables like options — always visible, never cleared
  • Function scope: variables like resp, status — visible inside the function, cleared on each new iteration
  • Block scope: variables in if/for/while blocks — visible only when paused inside that block, cleared on re-entry

When paused, only variables whose scope is an ancestor of the current pause point are shown. This prevents confusion from stale variables that are no longer in scope.


Reference

Preprocessor CLI
k6-debug-preprocess <input.js> [-o output.js] [--source-maps]
Flag Description
-o <file> Write output to a file instead of stdout
--source-maps Embed an inline source map in the output
Environment variables
Variable Description
K6_DEBUG_DAP Set to a host:port (e.g. :4711) to start the DAP server. Omit for CLI-only mode.
K6_AUTO_EXTENSION_RESOLUTION Set to false to prevent k6 from trying to auto-resolve the debug extension.
DAP commands supported
Command What it does
initialize Returns debugger capabilities
launch/attach Acknowledges the debug session
setBreakpoints Sets breakpoints by file and line
configurationDone Signals the IDE is ready; unblocks VU execution
threads Lists VUs as threads
stackTrace Returns the current pause location with VU identity
scopes Returns a "Local Variables" scope
variables Returns in-scope captured variables in source order
continue Resumes all VUs until next breakpoint
next Steps one line in the active VU; others stay frozen
source Returns file contents for the IDE
disconnect Resumes all VUs and closes the debug session
Running tests
# Go unit tests
cd xk6-debug/
go test -race ./...

# Preprocessor tests
cd preprocessor/
node src/plugin.test.js

Documentation

Overview

Package debug provides a k6 extension module for debugging JavaScript test scripts. It exports `capture` and `breakpoint` functions that are injected by the Babel preprocessor.

Usage:

import { capture, breakpoint } from 'k6/x/debug';

When K6_DEBUG_DAP environment variable is set (e.g., K6_DEBUG_DAP=:4711), a DAP server is started for IDE integration. Otherwise, debug output goes to stderr and breakpoints can be resumed via stdin.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BreakpointManager

type BreakpointManager struct {
	// contains filtered or unexported fields
}

BreakpointManager manages breakpoints and per-VU debug state.

func NewBreakpointManager

func NewBreakpointManager() *BreakpointManager

NewBreakpointManager creates a new BreakpointManager.

func (*BreakpointManager) EnsureVUState

func (bm *BreakpointManager) EnsureVUState(vuID uint64)

EnsureVUState initializes VU state if it doesn't exist.

func (*BreakpointManager) EnterScope

func (bm *BreakpointManager) EnterScope(vuID uint64, scopeID int, parentID int)

EnterScope registers a scope's parent relationship and clears any variables from a previous entry of this scope (e.g., previous loop iteration or function call).

func (*BreakpointManager) GetVUState

func (bm *BreakpointManager) GetVUState(vuID uint64) *VUDebugState

GetVUState returns the debug state for a VU.

func (*BreakpointManager) GetVisibleVariables

func (bm *BreakpointManager) GetVisibleVariables(vuID uint64) []VisibleVar

GetVisibleVariables returns variables that are in scope at the VU's current pause point, in the order they were first captured.

func (*BreakpointManager) HasAnyBreakpoints

func (bm *BreakpointManager) HasAnyBreakpoints() bool

HasAnyBreakpoints returns true if any breakpoints have been set via DAP.

func (*BreakpointManager) IsDisconnected

func (bm *BreakpointManager) IsDisconnected() bool

IsDisconnected returns true if the DAP client has disconnected.

func (*BreakpointManager) IsSet

func (bm *BreakpointManager) IsSet(file string, line int) bool

IsSet checks if a breakpoint is set at the given file and line. It first tries an exact path match, then falls back to basename matching to handle the mismatch between VS Code's absolute paths and the preprocessor's relative filenames.

func (*BreakpointManager) IsSteppingOver

func (bm *BreakpointManager) IsSteppingOver(vuID uint64, line int) bool

IsSteppingOver returns true if the VU is currently in a step operation and the given line is the same as where it last paused. This prevents breakpoints on the current line from re-triggering immediately after a step resumes.

func (*BreakpointManager) ListVUs

func (bm *BreakpointManager) ListVUs() []uint64

ListVUs returns all known VU IDs.

func (*BreakpointManager) ListenStdinResume

func (bm *BreakpointManager) ListenStdinResume()

ListenStdinResume reads lines from stdin and resumes all paused VUs on each line. This is the fallback mode when no DAP server is active.

func (*BreakpointManager) Pause

func (bm *BreakpointManager) Pause(ctx context.Context, vuID uint64, file string, line int, scopeID int, notify bool) ResumeAction

Pause blocks the VU goroutine until resumed or context is cancelled. If notify is true, the onStopped callback is invoked (sends DAP stopped event), but only if this VU is the first to stop in this round (allStopped not yet set). If another VU already triggered the stop, this VU pauses silently. Returns the resume action taken, or ActionContinue if context was cancelled.

func (*BreakpointManager) RemoveVU

func (bm *BreakpointManager) RemoveVU(vuID uint64)

RemoveVU cleans up state for a VU.

func (*BreakpointManager) RequestPauseAll

func (bm *BreakpointManager) RequestPauseAll()

RequestPauseAll sets the allStopped flag so all VUs pause at their next instrumented statement. Called when one VU hits a breakpoint.

func (*BreakpointManager) Resume

func (bm *BreakpointManager) Resume(vuID uint64, action ResumeAction)

Resume unblocks a paused VU with the given action.

func (*BreakpointManager) ResumeAll

func (bm *BreakpointManager) ResumeAll(action ResumeAction)

ResumeAll resumes all paused VUs and clears the stop flags.

func (*BreakpointManager) SetBreakpoints

func (bm *BreakpointManager) SetBreakpoints(file string, lines []int)

SetBreakpoints sets the active breakpoints for a file. Replaces any previous breakpoints for that file.

func (*BreakpointManager) SetDisconnected

func (bm *BreakpointManager) SetDisconnected()

SetDisconnected marks the client as disconnected and resumes all paused VUs. After this, Pause() becomes a no-op to prevent VUs from blocking.

func (*BreakpointManager) SetOnStopped

func (bm *BreakpointManager) SetOnStopped(fn func(vuID uint64, file string, line int))

SetOnStopped sets a callback invoked when a VU pauses at a breakpoint.

func (*BreakpointManager) SetOnThread

func (bm *BreakpointManager) SetOnThread(fn func(vuID uint64, reason string))

SetOnThread sets a callback invoked when a VU starts or exits.

func (*BreakpointManager) ShouldPauseAll

func (bm *BreakpointManager) ShouldPauseAll(vuID uint64) bool

ShouldPauseAll returns true if the VU should pause because another VU triggered a breakpoint (all-stop mode). Only returns true for VUs that aren't already paused.

func (*BreakpointManager) ShouldStepPause

func (bm *BreakpointManager) ShouldStepPause(vuID uint64, line int) bool

ShouldStepPause checks if the VU should pause due to a pending step (next/stepIn). It only triggers when the current line differs from where the VU was previously paused, so multiple captures on the same source line (e.g. variable + __line marker) don't cause duplicate pauses. Clears the stepping flag when it returns true.

func (*BreakpointManager) StoreVariable

func (bm *BreakpointManager) StoreVariable(vuID uint64, name string, value sobek.Value, scopeID int)

StoreVariable stores a captured variable for a VU with its scope ID.

type DAPVariable

type DAPVariable struct {
	Name               string
	Value              string
	Type               string
	VariablesReference int // >0 if expandable
}

DAPVariable represents a variable for the DAP protocol.

type ModuleInstance

type ModuleInstance struct {
	// contains filtered or unexported fields
}

ModuleInstance is the per-VU instance of the debug module.

func (*ModuleInstance) Breakpoint

func (mi *ModuleInstance) Breakpoint(location sobek.Value)

Breakpoint is called in place of `debugger` statements. location is {line, col, file, scope}.

func (*ModuleInstance) Capture

func (mi *ModuleInstance) Capture(location sobek.Value, value sobek.Value)

Capture is called after each instrumented variable assignment. It only stores the variable value — all pause logic is handled by Line(). location is {line, col, name, file, scope}, value is the JS variable value.

func (*ModuleInstance) EnterScope

func (mi *ModuleInstance) EnterScope(scopeID int, parentID int)

EnterScope registers a scope's parent relationship and clears stale variables from a previous entry of this scope (e.g., previous iteration or loop pass).

func (*ModuleInstance) Exports

func (mi *ModuleInstance) Exports() modules.Exports

Exports returns the named exports for `import { line, capture, breakpoint, enterScope } from 'k6/x/debug'`.

func (*ModuleInstance) Line

func (mi *ModuleInstance) Line(location sobek.Value)

Line is called at the start of each instrumented source line. It handles breakpoint detection and step-over logic, pausing the VU if needed. location is {line, col, file, scope}.

type ResumeAction

type ResumeAction int

ResumeAction represents the action to take when resuming a paused VU.

const (
	// ActionContinue resumes execution until next breakpoint.
	ActionContinue ResumeAction = iota
	// ActionNext steps to the next instrumented statement (behaves like continue for now).
	ActionNext
	// ActionStepIn steps into the next instrumented statement (behaves like continue for now).
	ActionStepIn
)

type RootModule

type RootModule struct {
	// contains filtered or unexported fields
}

RootModule is the top-level module, created once per test run.

func New

func New() *RootModule

New creates a new RootModule.

func (*RootModule) NewModuleInstance

func (rm *RootModule) NewModuleInstance(vu modules.VU) modules.Instance

NewModuleInstance creates a per-VU module instance.

type ScopedVariable

type ScopedVariable struct {
	Value   sobek.Value
	ScopeID int
}

ScopedVariable pairs a captured value with its lexical scope ID.

type VUDebugState

type VUDebugState struct {
	Resume     chan ResumeAction
	Paused     bool
	PauseLine  int
	PauseFile  string
	PauseScope int                       // scope ID at the pause point
	Variables  map[string]ScopedVariable // variable name → scoped value
	VarOrder   []string                  // insertion order of variable names
	LastAction ResumeAction              // the action that last resumed this VU
}

VUDebugState holds the debug state for a single VU.

type VariableStore

type VariableStore struct {
	// contains filtered or unexported fields
}

VariableStore manages DAP variablesReference IDs for expandable objects.

func NewVariableStore

func NewVariableStore() *VariableStore

NewVariableStore creates a new VariableStore.

func (*VariableStore) Add

func (vs *VariableStore) Add(obj *sobek.Object) int

Add stores a sobek object and returns a new reference ID.

func (*VariableStore) Clear

func (vs *VariableStore) Clear()

Clear resets the store between pauses.

func (*VariableStore) Get

func (vs *VariableStore) Get(refID int) *sobek.Object

Get returns the sobek object for a reference ID.

type VisibleVar

type VisibleVar struct {
	Name  string
	Value sobek.Value
}

VisibleVar is a variable name-value pair returned in capture order.

Directories

Path Synopsis
cmd
k6debug command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL