Skip to main content
This covers effective debugging methods using Naite.

What is Naite?

Naite is Sonamu’s test-dedicated logging system that collects and analyzes all logs generated during test execution. Key features:
  • Log isolation per test
  • Key-value based log storage
  • Powerful query capabilities
  • Visualization with Naite Viewer

Basic Usage

1. Recording Logs

import { Naite } from "sonamu";

describe("User API", () => {
  test("create user", async () => {
    const email = "test@example.com";

    // Record log
    Naite.t("user.create.input", { email });

    const user = await UserModel.create({ email });

    Naite.t("user.create.output", { userId: user.id });

    expect(user.email).toBe(email);
  });
});

2. Retrieving Logs

When a test fails, you can check the logs to identify the problem:
test("debugging example", async () => {
  try {
    const result = await complexOperation();
    expect(result).toBe(expectedValue);
  } catch (error) {
    // Check logs on failure
    const logs = Naite.getAll();
    console.log("Test logs:", logs);
    throw error;
  }
});

Debugging Patterns

API Request/Response Tracking

test("API call tracking", async () => {
  const params = { page: 1, limit: 10 };

  // Record request
  Naite.t("api.request", {
    method: "findMany",
    params
  });

  const users = await UserModel.findMany(params);

  // Record response
  Naite.t("api.response", {
    count: users.length,
    firstUser: users[0]
  });

  expect(users.length).toBeGreaterThan(0);
});

Database Query Tracking

test("query tracking", async () => {
  const userId = 1;

  // Before query
  Naite.t("db.query.before", { userId });

  const user = await UserModel.findById(userId);

  // After query
  Naite.t("db.query.after", {
    found: !!user,
    data: user
  });

  expect(user).toBeDefined();
});

Business Logic Flow Tracking

test("order process tracking", async () => {
  const orderId = 1;

  Naite.t("order.process.start", { orderId });

  // 1. Get order
  const order = await OrderModel.findById(orderId);
  Naite.t("order.found", { status: order.status });

  // 2. Process payment
  const payment = await processPayment(order);
  Naite.t("payment.processed", {
    paymentId: payment.id,
    amount: payment.amount
  });

  // 3. Update order status
  await OrderModel.updateStatus(orderId, "paid");
  Naite.t("order.status.updated", { newStatus: "paid" });

  Naite.t("order.process.complete", { orderId });
});

Log Key Naming Conventions

Designing keys with a hierarchical structure makes querying easier later:
// Good key naming
Naite.t("user.create.input", data);
Naite.t("user.create.validation", result);
Naite.t("user.create.output", user);

// Domain separation
Naite.t("order.payment.start", params);
Naite.t("order.payment.complete", result);

// Bad key naming
Naite.t("data1", data);
Naite.t("result", result);
Naite.t("test", value);
Recommended pattern:
{domain}.{action}.{stage}
Example: user.create.input, order.payment.complete

Conditional Logging

Activate logging only when debugging is needed:
const DEBUG = process.env.DEBUG === "true";

test("conditional logging", async () => {
  if (DEBUG) {
    Naite.t("debug.start", { timestamp: Date.now() });
  }

  const result = await someOperation();

  if (DEBUG) {
    Naite.t("debug.result", result);
  }

  expect(result).toBeDefined();
});

Using Naite in Models

You can log with Naite in business logic:
class UserModelClass extends BaseModel {
  @api()
  async createUser(name: string, email: string) {
    // Record log with Naite.t()
    Naite.t("user.input", { name, email });

    const user = await this.save({ name, email });

    Naite.t("user.created", user);

    return user;
  }
}
Check in tests:
test("verify Naite logs", async () => {
  await UserModel.createUser("Alice", "alice@example.com");

  // Extract first data with .first()
  const input = Naite.get("user.input").first();
  const created = Naite.get("user.created").first();

  expect(input.name).toBe("Alice");
  expect(created.id).toBeDefined();
});

Log Queries

Naite provides powerful query capabilities:
test("log queries", async () => {
  // Perform multiple operations
  await createUsers();
  await processOrders();

  // Query by specific key
  const userLogs = Naite.get("user").result();

  // Query by pattern (wildcard supported)
  const createLogs = Naite.get("*.create.*").result();

  // All logs
  const allLogs = Naite.getAll();

  console.log("User logs:", userLogs);
  console.log("Create logs:", createLogs);
  console.log("All logs:", allLogs);
});

NaiteQuery Methods

Naite.get() returns a NaiteQuery object:
test("NaiteQuery methods", async () => {
  Naite.t("test.data", { value: 1 });
  Naite.t("test.data", { value: 2 });
  Naite.t("test.data", { value: 3 });

  // First data
  const first = Naite.get("test.data").first();
  console.log(first); // { value: 1 }

  // Last data
  const last = Naite.get("test.data").last();
  console.log(last); // { value: 3 }

  // All data as array
  const all = Naite.get("test.data").result();
  console.log(all); // [{ value: 1 }, { value: 2 }, { value: 3 }]
});

Using Naite Viewer

Visualize logs with Naite Viewer after running tests:
# Run tests
pnpm test

# Run Naite Viewer
pnpm naite:view
Viewer features:
  • Filter logs by test
  • Key-based search
  • Timeline view
  • Export logs

Debugging Workflow

1. When Tests Fail

test("failing test", async () => {
  // Add sufficient logs
  Naite.t("test.start", { timestamp: Date.now() });

  try {
    const result = await problematicFunction();
    Naite.t("test.result", result);
    expect(result).toBe(expectedValue);
  } catch (error) {
    // Output all logs on error
    console.log("=== Naite Logs ===");
    console.log(JSON.stringify(Naite.getAll(), null, 2));
    throw error;
  }
});

2. Log Analysis

afterEach(() => {
  // Summarize logs after test
  const allLogs = Naite.getAll();
  const errorKeys = Object.keys(allLogs).filter(key =>
    key.includes("error") || key.includes("fail")
  );

  if (errorKeys.length > 0) {
    const errorLogs = errorKeys.map(key => ({ key, data: allLogs[key] }));
    console.log("Warning - Error logs:", errorLogs);
  }
});

3. Reproducing Specific Scenarios

test("bug reproduction", async () => {
  // Reproduce the scenario where the bug occurred
  Naite.t("bug.reproduction.start", {
    scenario: "Delete user immediately after creation"
  });

  const user = await UserModel.create({ email: "test@example.com" });
  Naite.t("bug.user.created", user);

  await UserModel.del(user.id);
  Naite.t("bug.user.deleted", { userId: user.id });

  // Attempt to retrieve again
  const deletedUser = await UserModel.findById(user.id);
  Naite.t("bug.user.found", { found: !!deletedUser });

  expect(deletedUser).toBeNull();
});

Performance Debugging

Add timestamps to logs for performance analysis:
test("performance measurement", async () => {
  const start = Date.now();
  Naite.t("perf.start", { timestamp: start });

  // Heavy operation
  const result = await heavyOperation();

  const end = Date.now();
  Naite.t("perf.end", {
    timestamp: end,
    duration: end - start,
    result
  });

  // Verify performance criteria
  expect(end - start).toBeLessThan(1000); // Within 1 second
});

Cautions

1. Do Not Use in Production Code

// Do not use Naite in production code
class UserModelClass extends BaseModel {
  async createUser() {
    Naite.t("user.create", data); // Bad - Test only!
    return this.save(data);
  }
}

2. Be Careful with Sensitive Information

test("logging cautions", async () => {
  // Do not log sensitive information like passwords
  Naite.t("user.create", {
    email,
    password  // Bad
  });

  // Exclude sensitive information
  Naite.t("user.create", {
    email,
    hasPassword: !!password  // Good
  });
});

3. Avoid Excessive Logging

// Excessive logging inside loops
for (let i = 0; i < 1000; i++) {
  Naite.t(`item.${i}`, data); // Bad - Too many
}

// Summary logging
Naite.t("items.processed", {
  count: 1000,
  sample: items.slice(0, 3)  // Good - Sample only
});