npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rodrigopsasaki/vision

v0.5.0

Published

Canonical context and structured observability for Node.js

Readme

CI License Version Downloads Install size TypeScript Coverage Open Issues

vision

Structured observability, modeled around intent — not output.


You don't need more logs, you need context.

You need to know:

  • What just happened
  • What data was involved
  • What the outcome was
  • Why it matters

But most systems log like this:


Three stages of observability

Let's say you're processing a shopping cart.

1. Naive logging

console.log("starting cart job");
console.log("loaded cart", cart.id);
console.log("charging", cart.total);
console.log("done", { status: "ok" });

This tells a story — but it's whispering. No IDs. No continuity. Just bursts of text into the void. Now imagine billions of these. Good luck finding the one you care about.


2. Disciplined logging

console.log("start", { user_id, correlation_id });
console.log("loaded cart", { cart_id, correlation_id });
console.log("charging", { amount, correlation_id });
console.log("done", { cart_id, status }); // <-- whoops, no user_id, no correlation_id

You're trying. You're threading a correlation_id. You're passing user_id everywhere manually. Until someone forgets. And now that one log line is invisible.


3. Structured context with Vision

await vision.observe("cart.process", async () => {
  vision.set("user_id", userId);
  vision.set("correlation_id", req.headers["x-correlation-id"]);

  const cart = await db.getCart(userId);
  vision.set("cart_id", cart.id);

  const result = await chargeCard(cart);
  vision.set("charge_status", result.status);

  await sendConfirmation(userId);
  vision.set("confirmation", "sent");
});

Now everything lives in a scoped context. No repetition. No missing keys. Just a clean canonical event:

{
  "name": "cart.process",
  "timestamp": "...",
  "data": {
    "user_id": "u123",
    "correlation_id": "abc-456",
    "cart_id": "c789",
    "charge_status": "success",
    "confirmation": "sent"
  }
}

Real-world usage

Vision is meant to disappear into your system. Here's what it looks like in real code:

await vision.observe("order.fulfillment", async () => {
  vision.set("user_id", user.id);
  vision.set("order_id", order.id);

  await fulfillOrder(order);
});
// fulfillment.ts
import { vision } from "@rodrigopsasaki/vision";

export async function fulfillOrder(order) {
  await pickItems(order);
  await packItems(order);
  await shipOrder(order);
}

async function pickItems(order) {
  // ...picking logic...
  vision.push("events", "picked");
}

async function packItems(order) {
  // ...packing logic...
  vision.push("events", "packed");
  vision.merge("dimensions", { weight: "2.1kg" }); // replaces a log line
}

async function shipOrder(order) {
  // ...shipping logic...
  vision.push("events", "shipped");
  vision.merge("shipment", {
    carrier: "DHL",
    tracking: "abc123",
  });
}

You don't pass context around. You don't log manually. You just describe what happened.

Vision collects it — then emits exactly one event.


Install

npm add @rodrigopsasaki/vision

Quick start

You don't need to configure anything to start using Vision.

import { vision } from "@rodrigopsasaki/vision";

await vision.observe("my.workflow", async () => {
  vision.set("step", "one");
  vision.set("status", "ok");
});

That's it. No setup. No boilerplate. No init() call required. Vision runs with a default console exporter out of the box.

When you're ready to customize behavior — like sending events to Datadog or disabling console logs — you can call vision.init() to register your own exporters.


Working with context

Vision gives you a few simple tools:

vision.set("foo", "bar");
vision.get("foo"); // "bar"

vision.push("tags", "new");
vision.push("tags", "priority");

vision.merge("meta", { version: "1.2.3" });
vision.merge("meta", { region: "us-east-1" });

Everything you set is scoped to the active observe() block. Accessing context outside that block throws — by design.


You don't need a correlation ID

Most systems bolt on correlation_id to make up for lost context. Vision doesn't lose context in the first place.

You don't thread a request ID. You don't decorate every log call. You just enter an observe() block — and Vision handles the scope.

Want to tag with a trace ID from upstream? Do it once:

await vision.observe("http.request", async () => {
  vision.set("trace_id", req.headers["x-trace-id"]);
  // do work
});

You get a unique ID for free. But you won't need it to hold everything together anymore.


Controlling output

By default, Vision logs a single event to the console. But you can register your own exporters.

Add exporters

Exporters are side-effect hooks that run at the end of a context — on success or error.

vision.registerExporter({
  name: "datadog",
  success: (ctx) => sendToDatadog(ctx),
  error: (ctx, err) => sendErrorToDatadog(ctx, err),
});

If you skip error(), Vision falls back to success() even on failure — so you'll still get the data.


Customize at startup

You can register exporters when you initialize:

vision.init({
  exporters: [
    {
      name: "stdout",
      success: (ctx) => {
        console.log("event:", ctx.name, Object.fromEntries(ctx.data.entries()));
      },
    },
  ],
});

You can also remove exporters later:

vision.unregisterExporter("stdout");

For now, the default console exporter can't be removed — but we'll probably support that soon.


Exporter Lifecycle Hooks

Exporters can optionally provide lifecycle hooks to wrap execution with custom logic. These hooks are purpose-built for observability systems.

Lifecycle Hooks Basics

Exporters can use lifecycle hooks to:

  • Set up their own context before execution (e.g., Datadog spans, OpenTelemetry traces)
  • Clean up or finalize after successful execution
  • Handle error-specific cleanup when execution fails
  • Still receive the final context data for export
const myExporter: VisionExporter = {
  name: "my-exporter",

  before: (ctx: VisionContext) => {
    // Set up your custom context here
    const span = tracer.startSpan(ctx.name);
    ctx.data.set("my.span", span);
  },

  after: (ctx: VisionContext) => {
    // Clean up after successful execution
    const span = ctx.data.get("my.span") as any;
    if (span) {
      span.finish();
      ctx.data.delete("my.span");
    }
  },

  onError: (ctx: VisionContext, err: unknown) => {
    // Handle error-specific cleanup
    const span = ctx.data.get("my.span") as any;
    if (span) {
      span.tags.error = err instanceof Error ? err.message : String(err);
      span.finish();
      ctx.data.delete("my.span");
    }
  },

  success: (ctx: VisionContext) => {
    // Export the final context data
    console.log("Success:", ctx);
  },
};

Lifecycle Hook Execution Order

  1. Before Phase: All exporter before() hooks are called in registration order
  2. Execution: The main callback function runs
  3. After Phase: If successful, all exporter after() hooks are called in registration order
  4. OnError Phase: If failed, all exporter onError() hooks are called in registration order
  5. Export: All exporter success() or error() functions are called

Real-world Example: Datadog Integration

import { vision } from "@rodrigopsasaki/vision";

const datadogExporter: VisionExporter = {
  name: "datadog",

  before: (ctx: VisionContext) => {
    // Create a Datadog span with vision context info
    const span = tracer.startSpan(ctx.name, {
      "vision.id": ctx.id,
      "vision.scope": ctx.scope || "unknown",
      "vision.source": ctx.source || "unknown",
    });

    // Store span in context for later cleanup
    ctx.data.set("datadog.span", span);
  },

  after: (ctx: VisionContext) => {
    // Finish span on successful completion
    const span = ctx.data.get("datadog.span") as any;
    if (span) {
      span.finish();
      ctx.data.delete("datadog.span");
    }
  },

  onError: (ctx: VisionContext, err: unknown) => {
    // Finish span with error information
    const span = ctx.data.get("datadog.span") as any;
    if (span) {
      span.tags.error = err instanceof Error ? err.message : String(err);
      span.finish();
      ctx.data.delete("datadog.span");
    }
  },

  success: (ctx: VisionContext) => {
    // Send metrics to Datadog
    sendMetrics(ctx.name, Object.fromEntries(ctx.data.entries()));
  },

  error: (ctx: VisionContext, err: unknown) => {
    // Send error metrics to Datadog
    sendErrorMetrics(ctx.name, err);
  },
};

// Register the exporter
vision.init({
  exporters: [datadogExporter],
});

// Now every observe() call creates a Datadog span automatically
await vision.observe("order.processing", async () => {
  vision.set("user_id", "user123");
  // ... work happens ...
});

Error Handling

Lifecycle hooks are executed appropriately based on the execution outcome. Error handling is declarative and clear:

const exporter: VisionExporter = {
  name: "error-handling",

  before: (ctx: VisionContext) => {
    console.log("Setting up...");
  },

  after: (ctx: VisionContext) => {
    console.log("Cleaning up after success...");
  },

  onError: (ctx: VisionContext, err: unknown) => {
    console.log("Cleaning up after error:", err);
  },

  success: (ctx: VisionContext) => {
    console.log("Success!");
  },
};

API Reference

Core Functions

vision.observe(options, callback)

Creates a new vision context and executes the provided callback within it.

Parameters:

  • options - Either a string (context name) or a full options object
  • callback - The async function to execute within the vision context

Returns: Promise that resolves to the callback result

Example:

// Simple usage
await vision.observe("user.login", async () => {
  vision.set("user_id", "user123");
  // ... work happens ...
});

// Advanced usage
await vision.observe(
  {
    name: "order.processing",
    scope: "http",
    source: "api-gateway",
    initial: { request_id: "req-123" },
  },
  async () => {
    vision.set("order_id", "order456");
    // ... work happens ...
  },
);

vision.init(options?)

Initializes the vision runtime with custom configuration.

Parameters:

  • options - Optional runtime configuration

Example:

vision.init({
  exporters: [
    {
      name: "datadog",
      success: (ctx) => sendToDatadog(ctx),
      error: (ctx, err) => sendErrorToDatadog(ctx, err),
    },
  ],
});

Context Manipulation

vision.set(key, value)

Sets a key-value pair in the current vision context.

Parameters:

  • key - The key to set
  • value - The value to store

vision.get(key)

Retrieves a value from the current vision context.

Parameters:

  • key - The key to retrieve

Returns: The stored value or undefined if not found

vision.push(key, value)

Pushes a value to an array in the current vision context.

Parameters:

  • key - The key for the array
  • value - The value to push

vision.merge(key, value)

Merges an object into an existing object in the current vision context.

Parameters:

  • key - The key for the object
  • value - The object to merge

vision.context()

Gets the current active vision context.

Returns: The current vision context

Throws: Error if called outside of a vision context

Exporter Management

vision.registerExporter(exporter)

Registers a new exporter to receive vision context data.

Parameters:

  • exporter - The exporter to register

vision.unregisterExporter(name)

Unregisters an exporter by name.

Parameters:

  • name - The name of the exporter to remove

Philosophy

Vision replaces many logs with one idea:

"This happened. Here's everything we know."

It's not a log formatter. It's a structured, observable boundary.

You stop logging every heartbeat. You start capturing truth.


Requirements

  • Node.js 18+ (for crypto.randomUUID() and AsyncLocalStorage)
  • TypeScript 5.0+ (for best experience)

License

MIT © Rodrigo Sasaki