Skip to main content
Learn how to record data during test execution using Naite.t().

Naite.t() Overview

Simple API

Two parametersIntuitive usage

Automatic Tracking

Automatic callstack collectionCall path identification

Any Type

All values allowedFlexible logging

Test-Only

NODE_ENV checkNo production impact

What is Naite.t()?

Naite.t() is a function that records data at specific points during test execution. It takes a unique key as the first argument and the value to record as the second argument.
import { Naite } from "sonamu";

// Basic usage
Naite.t("user:create", { userId: 123, username: "john" });
Data recorded this way:
  • Can be queried at any time during test execution with Naite.get()
  • Transmitted in real-time to VSCode Extension
  • Stored with callstack and time information

Basic Usage

Simple Logging

The most basic usage is recording function inputs and outputs.
test("user creation flow", async () => {
  const userModel = new UserModel();

  // Record input data
  const input = {
    username: "john",
    email: "john@example.com",
  };
  Naite.t("user:create:input", input);

  // Create user
  const { user } = await userModel.create(input);

  // Record output data
  Naite.t("user:create:output", {
    userId: user.id,
    username: user.username,
  });
});
Input/Output Pattern: Using :input/:output or :before/:after patterns helps clearly track data transformation processes.

Step-by-Step Logging

Track the flow of complex processes by logging step by step.
test("full order processing flow", async () => {
  const orderId = 123;

  // Step 1: Order validation
  Naite.t("order:validate:start", { orderId });
  await validateOrder(orderId);
  Naite.t("order:validate:done", { valid: true });

  // Step 2: Inventory check
  Naite.t("order:inventory:check", { orderId });
  const available = await checkInventory(orderId);
  Naite.t("order:inventory:result", { available });

  // Step 3: Payment processing
  Naite.t("order:payment:start", { orderId });
  await processPayment(orderId);
  Naite.t("order:payment:done", { transactionId: "tx_123" });

  // Step 4: Start shipping
  Naite.t("order:shipping:start", { orderId });
  await startShipping(orderId);
  Naite.t("order:shipping:done", { trackingNumber: "TRK_001" });
});
Benefits:
  • Clearly identify which step failed
  • Measure time spent at each step
  • Filter specific steps with wildcard patterns (order:payment:*)
start/done Pattern: Recording the start and completion of each operation with :start/:done makes it easier to calculate elapsed time or verify completion status later.

Understanding Internal Behavior

1. Environment Check

Naite.t() first checks the execution environment.
t(name: string, value: any) {
  // Immediately return if not in test environment
  if (process.env.NODE_ENV !== "test") {
    return;
  }
  // ...
}
Reasons:
  • Safe even if Naite.t() exists in production code
  • Test-only feature
  • No runtime overhead
You don’t need to remove Naite.t() calls before production deployment. It’s automatically controlled by environment variables.

2. Context Check

Naite uses Sonamu’s Context system.
try {
  const context = Sonamu.getContext();
  const store = context?.naiteStore;

  if (!store) {
    return; // Ignore if no Context
  }
  // ...
} catch {
  // No Context situation
}
Role of Context:
  • Provides independent naiteStore for each test
  • Ensures test isolation
  • Maintains Context in async operations with AsyncLocalStorage
// bootstrap.ts
function getMockContext(): Context {
  return {
    ip: "127.0.0.1",
    session: {},
    user: null,
    naiteStore: Naite.createStore(), // Create new Map
    // ...
  };
}

// New Context for each test() execution
export const test = async (title, fn, options) => {
  return vitestTest(title, options, async (context) => {
    await runWithMockContext(async () => {
      // Naite.t() can be called here
      await fn(context);
    });
  });
};

3. Callstack Collection

Automatically collects the callstack at the time of Naite.t() call.
// Callstack collection
const stack = extractCallStack();

function extractCallStack(): StackFrame[] {
  const stack = new Error().stack;

  // Exclude "Error", "extractCallStack", "Naite.t"
  const frames = stack.split("\n")
    .slice(3)
    .map(parseStackFrame)
    .filter(frame => frame !== null);

  // Stop when runWithContext is found
  const contextIndex = frames.findIndex(
    f => f.functionName?.includes("runWithContext")
  );

  return contextIndex >= 0
    ? frames.slice(0, contextIndex + 1)
    : frames;
}
Collected Information:
  • Function name (createUser)
  • File path (/Users/.../user.model.ts)
  • Line number (123)
async function createUser() {
  Naite.t("user:create", { username: "john" });
}

test("create user", async () => {
  await createUser();
});

// Collected callstack:
// [
//   {
//     functionName: "createUser",
//     filePath: "/Users/.../user.model.ts",
//     lineNumber: 15
//   },
//   {
//     functionName: "test",
//     filePath: "/Users/.../user.model.test.ts",
//     lineNumber: 42
//   },
//   {
//     functionName: "runWithMockContext",
//     filePath: "/Users/.../bootstrap.ts",
//     lineNumber: 58
//   }
// ]

4. Trace Creation and Storage

Creates a NaiteTrace object with collected information and stores it in the Store.
const trace: NaiteTrace = {
  key: name,
  data: value,
  stack: stack,
  at: new Date(),
};

// Add to Store (always managed as array)
const existing = store.get(name) ?? [];
store.set(name, [...existing, trace]);
Reason for Array Management:
  • Same key can be called multiple times
  • Track in chronological order
  • Support for loop operation logging
Naite.t("user:create", { userId: 1 });

// Store:
// "user:create" => [{ key, data: { userId: 1 }, ... }]

Key Naming Strategy

Hierarchical Structure

Separating hierarchies with colons (:) makes it easy to query later with wildcard patterns.
// βœ… Hierarchical structure
Naite.t("user:create", { /* ... */ });
Naite.t("user:update", { /* ... */ });
Naite.t("user:delete", { /* ... */ });

Naite.t("syncer:renderTemplate", { /* ... */ });
Naite.t("syncer:writeFile", { /* ... */ });

// Easy query with wildcards
Naite.get("user:*").result();         // All user related
Naite.get("syncer:*").result();       // All syncer related
Naite.get("*:create").result();       // All create operations
1

module:function

The most basic pattern.
Naite.t("user:create", { /* ... */ });
Naite.t("post:update", { /* ... */ });
Naite.t("syncer:render", { /* ... */ });
2

module:function:action

Use when more detailed tracking is needed.
Naite.t("user:create:input", { /* ... */ });
Naite.t("user:create:validation", { /* ... */ });
Naite.t("user:create:db", { /* ... */ });
Naite.t("user:create:done", { /* ... */ });
3

module:function:action:detail

Use for very complex flows.
Naite.t("order:payment:card:charge:start", { /* ... */ });
Naite.t("order:payment:card:charge:done", { /* ... */ });
Naite.t("order:payment:bank:transfer:start", { /* ... */ });
Naite.t("order:payment:bank:transfer:done", { /* ... */ });
Too deep hierarchies (5+ levels) actually reduce readability. In most cases, 3 levels are sufficient.

Practical Patterns

1. Conditional Tracking

Clearly track business logic branches.
async function processPayment(order: Order) {
  Naite.t("payment:start", {
    orderId: order.id,
    method: order.payment_method
  });

  if (order.payment_method === "card") {
    Naite.t("payment:card:selected", { cardNumber: "****1234" });
    await chargeCard(order);
    Naite.t("payment:card:success", { transactionId: "tx_123" });
  } else if (order.payment_method === "bank") {
    Naite.t("payment:bank:selected", { bankCode: "001" });
    await transferBank(order);
    Naite.t("payment:bank:success", { transferId: "tf_456" });
  }

  Naite.t("payment:done", { orderId: order.id });
}

test("card payment", async () => {
  const order = { id: 1, payment_method: "card" };
  await processPayment(order);

  // Verify card payment path was executed
  const cardLogs = Naite.get("payment:card:*").result();
  expect(cardLogs.length).toBeGreaterThan(0);

  // Bank transfer was not executed
  const bankLogs = Naite.get("payment:bank:*").result();
  expect(bankLogs).toHaveLength(0);
});
Usage:
  • A/B testing path verification
  • Permission branch verification
  • Error handling path tracking

2. Error Tracking

Record detailed information when errors occur.
async function createUser(data: UserCreateInput) {
  Naite.t("user:create:input", data);

  try {
    // Validation
    await validateUser(data);
    Naite.t("user:create:validation:pass", {});

    // DB save
    const user = await db.insert("users").values(data);
    Naite.t("user:create:success", { userId: user.id });

    return user;
  } catch (error) {
    // Record error details
    Naite.t("user:create:error", {
      errorType: error.constructor.name,
      errorMessage: error.message,
      errorCode: error.code,
      inputData: data,
      stack: error.stack,
    });
    throw error;
  }
}

test("invalid input handling", async () => {
  try {
    await createUser({ username: "" }); // Empty value
    throw new Error("Should have failed");
  } catch (error) {
    // Check error log
    const errorLog = Naite.get("user:create:error").first();

    expect(errorLog).toBeDefined();
    expect(errorLog.errorType).toBe("ValidationError");
    expect(errorLog.errorMessage).toContain("username");

    // Verify validation was not passed
    const validationPass = Naite.get("user:create:validation:pass").result();
    expect(validationPass).toHaveLength(0);
  }
});
Error Tracking Pattern: Calling Naite.t() in the catch block allows you to capture all context at the time of error. Combined with callstack storage, debugging becomes much easier.

3. Performance Measurement

Record time information to identify bottlenecks.
test("data processing performance", async () => {
  Naite.t("process:start", { timestamp: Date.now() });

  // Step 1: Data fetch
  Naite.t("process:fetch:start", { timestamp: Date.now() });
  const data = await fetchLargeData();
  Naite.t("process:fetch:done", {
    timestamp: Date.now(),
    dataSize: data.length
  });

  // Step 2: Data processing
  Naite.t("process:transform:start", { timestamp: Date.now() });
  const processed = await transformData(data);
  Naite.t("process:transform:done", {
    timestamp: Date.now(),
    processedCount: processed.length
  });

  // Step 3: DB save
  Naite.t("process:save:start", { timestamp: Date.now() });
  await saveToDatabase(processed);
  Naite.t("process:save:done", { timestamp: Date.now() });

  Naite.t("process:end", { timestamp: Date.now() });

  // Calculate time for each step
  const logs = Naite.get("process:*").result();
  const start = logs.find(l => l.timestamp).timestamp;
  const fetchDone = logs.find(l => l.timestamp && l.dataSize).timestamp;
  const transformDone = logs.find(l => l.timestamp && l.processedCount).timestamp;
  const saveDone = logs[logs.length - 2].timestamp;

  console.log(`Fetch: ${fetchDone - start}ms`);
  console.log(`Transform: ${transformDone - fetchDone}ms`);
  console.log(`Save: ${saveDone - transformDone}ms`);
});

4. Loop Operation Tracking

For loops or batch operations, record only summary information.
// ❌ Logging inside loop
for (let i = 0; i < 10000; i++) {
  Naite.t("loop:iteration", { i, value: data[i] });
  // Creates 10,000 traces!
}

Value Types and Serialization

Any Type Allowed

Naite.t() accepts any type, allowing all JavaScript values to be recorded.
// βœ… All types possible
Naite.t("key", "string");
Naite.t("key", 123);
Naite.t("key", true);
Naite.t("key", { nested: { object: true } });
Naite.t("key", [1, 2, 3]);
Naite.t("key", new Date());
Naite.t("key", null);
Naite.t("key", undefined);
Design Reasons:
  • Ease of use prioritized over TypeScript type safety
  • No interruption from type errors while writing tests
  • Same philosophy as expect()

Serialization Warning

JSON serialization is required to transmit to VSCode Extension. Non-serializable values will show a warning.
╔════════════════════════════════════════════════════════════════╗
β•‘  [Naite] Non-serializable value detected!                      β•‘
╠════════════════════════════════════════════════════════════════╣
β•‘  Key: user:create                                               β•‘
β•‘  Reason: Cannot serialize function                              β•‘
β•‘  Location: /Users/.../user.model.test.ts                        β•‘
β•‘  Line: 15                                                       β•‘
╠════════════════════════════════════════════════════════════════╣
β•‘  Naite.t() accepts any type of value. However, values will     β•‘
β•‘  be serialized to JSON when exported via Naite.getAllTraces(). β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
Tests run normally even with warnings. However, you won’t be able to see those values in VSCode Extension.

Performance Considerations

1. Test Environment Only

if (process.env.NODE_ENV !== "test") {
  return; // Immediate return, no cost
}
Even if Naite.t() exists in production code, there’s no performance impact.

2. Avoid Excessive Logging

Calling Naite.t() inside loops creates thousands of traces, causing:
  • Increased memory usage
  • Slower test speed
  • Slower VSCode Extension
Recommendations:
  • Record only loop start/end
  • Record only when problems occur
  • Use summary information

3. Conditional Logging

Enable detailed logging only when debugging is needed.
const DEBUG = process.env.DEBUG_NAITE === "true";

function processItem(item: any) {
  if (DEBUG) {
    Naite.t("process:detail", item);
  }

  // Processing logic
}

// Usage:
// DEBUG_NAITE=true pnpm test

Best Practices

1

Use Clear Keys

// βœ… Correct way
Naite.t("user:create:input", { username: "john" });
Naite.t("user:create:validation", { valid: true });
Naite.t("user:create:db", { query: "INSERT..." });

// ❌ Wrong way
Naite.t("data1", { username: "john" });
Naite.t("data2", { valid: true });
Naite.t("data3", { query: "INSERT..." });
2

Consistent Structure

// βœ… Correct way: Consistent hierarchy
Naite.t("user:create", { /* ... */ });
Naite.t("user:update", { /* ... */ });
Naite.t("user:delete", { /* ... */ });

Naite.t("post:create", { /* ... */ });
Naite.t("post:update", { /* ... */ });
Naite.t("post:delete", { /* ... */ });
3

Minimal Information

// βœ… Correct way: Only necessary information
Naite.t("user:create", {
  userId: user.id,
  username: user.username,
});

// ❌ Wrong way: Including unnecessary information
Naite.t("user:create", {
  ...user,              // All fields
  ...request,           // Entire Request
  ...context,           // Entire Context
});
4

Log at Meaningful Locations

// βœ… Correct way: At important branch points
async function processUser(user: User) {
  Naite.t("user:process:start", { userId: user.id });

  if (user.isAdmin) {
    Naite.t("user:process:admin", { userId: user.id });
    await processAdmin(user);
  } else {
    Naite.t("user:process:regular", { userId: user.id });
    await processRegular(user);
  }

  Naite.t("user:process:done", { userId: user.id });
}

Cautions

Cautions when using Naite.t():
  1. Test-only: Only works when NODE_ENV === "test".
  2. Context required: Sonamu.getContext() must exist. Only use within bootstrap’s runWithMockContext().
  3. Serializable values recommended: Serializable values are recommended for VSCode Extension transmission.
  4. No excessive logging: Calling inside loops degrades performance.
  5. Key convention: The module:function:action format is recommended.

Next Steps