Skip to content

samirg1/eventflowjs

Repository files navigation

EventFlow

EventFlow logo

NPM Downloads Weekly NPM Version Dependencies

EventFlow is a lightweight TypeScript library for event lifecycle logging.

Instead of scattering many log statements, you create one event, enrich it as work progresses, and emit a final structured log when complete.

Why EventFlow

  • Lifecycle-oriented logging (start -> enrich -> step -> end)
  • Structured JSON output for easy ingestion and searching
  • Works in Node.js, browsers, and fullstack flows
  • Async-safe event isolation on Node via AsyncLocalStorage
  • 0 dependencies

Installation

npm install eventflowjs

Examples Quickstart

Run a native / web version to try it out here:

Quick Start

import { EventFlow } from "eventflowjs";

EventFlow.startEvent("createUser");
EventFlow.addContext({ userId: 123, email: "test@example.com" });
EventFlow.step("create-db-record");
EventFlow.step("send-email");
EventFlow.endEvent();

Custom Transports

import { ConsoleTransport, EventFlow, Transport } from "eventflowjs";

class HttpTransport extends Transport {
    log(event: Transport.EventLog): void {
        void fetch("/logs", {
            method: "POST",
            headers: { "content-type": "application/json" },
            body: JSON.stringify(event),
        });
    }
}

EventFlow.configure({
    transports: [
    new ConsoleTransport({
        emissionMode: "errors-only",
        nonErrorSampleRate: 100,
        debug: true,
    }),
    new HttpTransport({ nonErrorSampleRate: 25 }),
    ],
});

emissionMode defaults to "all" and can be set to "errors-only". nonErrorSampleRate defaults to 100 and controls what percentage of non-failed events are emitted. debug defaults to false. When true, suppressed successful events trigger a simple Successful Event debug message.

Example Fullstack Flow w/ Stripe

Send event to API

// client/src/components/CartCheckout.tsx
EventFlow.startEvent("user_checkout");
EventFlow.step("client.user_press_checkout");
EventFlow.addContext({ cartID });
EventFlow.addUserContext(user); // see configuration
EventFlow.step("client.create_payment_intent");
const { paymentIntentID, clientSecret, continuationToken } = await fetch(
    "/api/createPaymentIntent",
    {
        method: "POST",
        body,
        headers: EventFlow.getPropagationHeaders(),
    },
);

Receive event from Client, prepare metadata for Webhook, send event back to Client

// api/index.ts
import { eventFlowMiddleware } from "eventflowjs";
app.use(eventFlowMiddleware);

// api/routes/createPaymentIntent.ts
app.post("/createPaymentIntent", async (req, res) => {
    const body = req.body;
    EventFlow.step("api.received_create_pi")
    EventFlow.addContext({ body });

    const metadata = EventFlow.getPropagationMetadata();
    const paymentIntent = await stripe.paymentIntents.create({
        ...
        metadata,
        ...
    });

    const clientSecret = process.env.STRIPE_CLIENT_SECRET;
    const paymentIntentID = paymentIntent.id;

    EventFlow.step("api.sending_back_to_client");
    EventFlow.addContext({ paymentIntentID });
    EventFlow.addEncryptedContext({ clientSecret })
    const continuationToken = EventFlow.getContinuationToken();
    res.json({ paymentIntentID, clientSecret, continuationToken });
});

Receive Event from API

// client/src/components/CartCheckout.tsx
EventFlow.continueFromToken(continuationToken);
EventFlow.addContext({ paymentIntentID });
EventFlow.step("client.present_payment_sheet");
const { error } = await presentPaymentSheet(paymentIntentID, clientSecret);
if (error) return handleError(error);

EventFlow.step("client.payment_success");
EventFlow.endEvent();

Receive event from Webhook

// api/routes/webhook
if (event.type === "payment_intent.succeeded") {
    const paymentIntent = event.data.object;

    EventFlow.fromMetadata(paymentIntent.metadata);
    EventFlow.step("webhook.payment_successful");
    EventFlow.addContext({ receiptNumber });
    EventFlow.endEvent();

    res.json({ received: true });
}

Final Output:

Note 3 emissions

  • client side
  • API route
  • API webhook
{
  "id": "evt_abc123",
  "name": "user_checkout",
  "status": "success",
  "timestamp": "2026-03-02T12:00:00.000Z",
  "duration_ms": 4678, // for webhook, client/api will be shorter
  "context": {
    "cartID": "abc123",
    "user": {
        "userID": "abc123",
        "email": "test@example.com",
    },
    "body": {
        "amount": 2500,
        "currency": "AUD"
    },
    "paymentIntentID": "pi_12345",
    "receiptNumber": "r_12345", // webhook only
  },
  "encryptedContext": {
    "clientSecret": "pi_secret_12345" // api/client only
  },
  "steps": [
    { "name": "client.user_press_checkout", "t": 30 },
    { "name": "client.create_payment_intent", "t": 60 }
    { "name": "api.received_create_pi", "t": 686 }
    { "name": "api.sending_back_to_client", "t": 959 } // api/client only
    { "name": "client.present_payment_sheet", "t": 1678 } // client only
    { "name": "client.payment_success", "t": 3652 } // client only
    { "name": "webhook.payment_successful", "t": 4678 } // webhook only
  ],
  "caller": { "file": "CartCheckout.tsx", "line": 42, "function": "onPressCheckout" },
  "traceId": "trc_xyz"
}

// if an error occurred
{
    ...
    "status": "failed",
    "context": {
        ...
        "customErrorContext": ...
    },
    "error": {
        "message": "payment declined",
        "stack": ...
    }
    ...
}

Client Configuration

Use configure to control client-level behavior:

import { EventFlow, type EventFlowClient } from "eventflowjs";

// user / account object on your platform
interface User { uid: string; email: string, ... };

// for typing `getUserContext`
const AppEventFlow: EventFlowClient<User> = EventFlow;

AppEventFlow.configure({
  showFullErrorStack: false,
  branding: false,
  encryptionKey: "shared-eventflow-key",
  getUserContext: (user) => ({
    email: user.email,
    id: user.uid,
  }),
});

export { AppEventFlow as EventFlow };

// then later:
EventFlow.startEvent("checkout");
EventFlow.addUserContext(user); // type safe
EventFlow.endEvent();

showFullErrorStack defaults to true. When set to false, emitted failed events include only the first two lines of error.stack. branding defaults to true. When set to false, ConsoleTransport logs raw JSON without the [EventFlow] prefix. transports optionally replaces active transport(s) in the same configure call (equivalent to calling setTransport(...)). encryptionKey is optional. When set, encryptedContext is encrypted in propagation headers, continuation tokens, and propagation metadata, then decrypted again in fromHeaders, continueFromToken, and fromMetadata. getUserContext configures addUserContext(account) to map your app-level user/account object into context.user. When context.user already exists, addUserContext overwrites it and logs a warning.

TypeScript note: assertion-based narrowing requires an explicitly typed local reference (for example, const AppEventFlow: EventFlowClient<User> = EventFlow;). Configure and use that reference in the same scope for typed addUserContext(...) calls.

Encrypted Context

Use addEncryptedContext when fields should stay readable in emitted logs but be encrypted while crossing service boundaries.

EventFlow.configure({ encryptionKey: "shared-eventflow-key" });

EventFlow.startEvent("checkout");
EventFlow.addContext({ cartId: "cart_1001" });
EventFlow.addEncryptedContext({
  customerEmail: "test@example.com",
  paymentIntentSecret: "pi_secret_123",
});

const headers = EventFlow.getPropagationHeaders();
// `headers` now contain encrypted `encryptedContext` values.

EventFlow.fromHeaders(headers);
console.log(EventFlow.getCurrentEvent()?.encryptedContext.paymentIntentSecret);
// "pi_secret_123"

context and encryptedContext are separate event fields. EventFlow encrypts only the values inside encryptedContext; key names remain visible so downstream services know which fields belong in the encrypted bucket.

Run Helper

EventFlow.run wraps a function call in a lifecycle-aware step. It catches errors, records failure in the event, and rethrows the error for normal propagation.

await EventFlow.run("payment", async (event) => {
    await payment();
});

Supported call signatures:

await EventFlow.run("step-name", async (event) => { ... }, options);
await EventFlow.run(async (event) => { ... }, options);

Useful options:

  • failEventOnError (default true): call EventFlow.fail(error) before rethrowing.
  • startIfMissing (default false): auto-start an event if none exists.
  • eventName: event name used when startIfMissing starts a new event.
  • endIfStarted (default true): auto-end only the event started by this run.
  • statusOnAutoEnd (default "success"): status used when auto-ending.

Instrument Helper

EventFlow.instrument creates a reusable wrapped function with the same error behavior as run.

const createUser = EventFlow.instrument("createUser", async (data) => {
    return db.createUser(data);
});

const user = await createUser({ email: "test@example.com" });

Defaults are wrapper-friendly:

  • auto-start event when missing
  • auto-end if the wrapper started it
  • fail + rethrow on error

Optional instrument options:

  • all run options (failEventOnError, startIfMissing, eventName, endIfStarted, statusOnAutoEnd)
  • stepName: override the step recorded for each call
  • contextFromArgs(...args): add context derived from input args
  • contextFromResult(result, ...args): add context from the returned value

React Native

If you're having issues in React-Native you can import from eventflowjs/react-native, and raise an issue to get it sorted.

  • import { EventFlow } from "eventflowjs/react-native";

API Reference

Primary Exports

Export Description
EventFlow Singleton EventFlowClient instance used for lifecycle logging.
EventFlowClient Class implementation behind the EventFlow singleton.
eventflowjs/react-native React Native entrypoint that exports EventFlow wired to browser-style in-memory context.
eventFlowMiddleware Ready-to-use Node/Express middleware (app.use(eventFlowMiddleware)).
ConsoleTransport Built-in JSON console transport.
Transport Base class for custom transports. Extend it and implement log(event).

Utility Functions

Function Description Arguments Returns
createEventFlowMiddleware(client, options?) Factory for custom middleware behavior. client: compatible EventFlow client, options?: EventFlowMiddlewareOptions EventFlowMiddleware
serializeEvent(event, options?) Serializes an event payload for transport. event: EventLog, options?: { encryptionKey?: string } string
deserializeEvent(data, options?) Parses serialized propagation payload safely. data: string, options?: { encryptionKey?: string } SerializedPropagationEvent or null
getPropagationHeaders(event, options?) Builds header propagation map from event. event: EventLog, options?: { encryptionKey?: string } Record<string, string>
extractEventFromHeaders(headers, options?) Rehydrates propagation payload from headers. headers: HeadersLike, options?: { encryptionKey?: string } SerializedPropagationEvent or null
getPropagationMetadata(event, options?) Builds metadata propagation map from event. event: EventLog, options?: PropagationMetadataOptions PropagationMetadata
extractEventFromMetadata(metadata, options?) Rehydrates propagation payload from metadata map. metadata: PropagationMetadataInput, options?: { encryptionKey?: string } SerializedPropagationEvent or null

EventFlow Methods

Method Description Arguments Returns
startEvent(name) Starts a new event. Auto-cancels and emits any currently active event first. name: string EventLog
addContext(data) Shallow-merges context into the active event. No-op if no active event exists. data: EventContext void
addEncryptedContext(data) Shallow-merges data into encryptedContext. Throws if encryptionKey is not configured. No-op if no event is active. data: EventContext void
addUserContext(account) Maps a configured user/account object and writes it to context.user. Throws if getUserContext is not configured. No-op if no event is active. account: TAccount void
step(name) Appends a step with elapsed time from event start. name: string void
endEvent(status?) Completes and emits the active event. status?: EventStatus (default "success") EventLog or null
fail(error) Marks active event as failed, captures error, emits, clears current event. error: unknown EventLog or null
configure(options) Updates client-level behavior settings. options: EventFlowClientConfigureOptions void
getCurrentEvent() Returns current active event in context. none EventLog or null
setTransport(transport) Replaces transport(s) used for emitting events. transport: Transport or Transport[] void
getPropagationHeaders() Builds propagation headers from active event. none Record<string, string>
fromHeaders(headers) Rehydrates and attaches event from propagation headers. headers: HeadersLike EventLog or null
attach(event) Attaches a provided event payload as current active event. event: EventLog or SerializedPropagationEvent EventLog
getContinuationToken(event?) Serializes an event for server->client or worker continuation. event?: EventLog (defaults to current event) string or null
continueFromToken(token) Restores and attaches an event from a continuation token. token: string EventLog or null
getPropagationMetadata(event?, options?) Produces provider-friendly metadata fields for continuation. event?: EventLog, options?: PropagationMetadataOptions PropagationMetadata
fromMetadata(metadata) Restores and attaches an event from metadata fields. metadata: PropagationMetadataInput EventLog or null
run(stepName?, fn, options?) Runs callback with optional step, captures+rethrows errors, optional auto-start/auto-end behavior. overloads: run(fn, options?), run(stepName, fn, options?) Promise<T>
instrument(eventName, fn, options?) Wraps a function in event lifecycle instrumentation for reuse. eventName: string, fn: (...args) => T, options?: InstrumentOptions (...args) => Promise<T>

RunOptions

Option Type Default Description
failEventOnError boolean true Calls EventFlow.fail(error) before rethrow when callback throws.
startIfMissing boolean false Auto-starts an event if none is active.
eventName string inferred Event name used when auto-starting.
endIfStarted boolean true Auto-ends event only if this run started it.
statusOnAutoEnd EventStatus "success" Status used for auto-end path.

EventFlowClientConfigureOptions

Option Type Default Description
showFullErrorStack boolean true When false, failed events include only the first two lines of error.stack.
branding boolean true When false, ConsoleTransport logs plain JSON without the branding prefix.
transports Transport | Transport[] n/a Replaces active transport(s), same as calling setTransport(...).
encryptionKey string n/a Shared symmetric key used to encrypt encryptedContext during propagation.

EventFlowClientConfigureWithUserContext<TAccount>

Option Type Default Description
showFullErrorStack boolean true Same as EventFlowClientConfigureOptions.
branding boolean true Same as EventFlowClientConfigureOptions.
transports Transport | Transport[] n/a Same as EventFlowClientConfigureOptions; also works alongside getUserContext.
encryptionKey string n/a Same as EventFlowClientConfigureOptions; encrypts encryptedContext during propagation.
getUserContext (account: TAccount) => EventContext required Maps your user/account object into the payload used by addUserContext(account) at context.user.

TransportEmissionOptions

Option Type Default Description
emissionMode "all" | "errors-only" "all" Controls whether all events are emitted or only failed events.
nonErrorSampleRate number 100 Percentage (0-100) of non-failed events to emit.
debug boolean false When enabled, suppressed successful events trigger a Successful Event debug message.

InstrumentOptions

InstrumentOptions extends RunOptions and adds:

Option Type Description
stepName string Step name recorded for each instrumented call (defaults to eventName).
contextFromArgs (...args) => EventContext Adds context from function input arguments.
contextFromResult (result, ...args) => EventContext Adds context from function result.

Middleware

Export Description Arguments
eventFlowMiddleware Default middleware instance using global EventFlow. (req, res, next)
createEventFlowMiddleware(client, options?) Creates middleware around any compatible client (startEvent, addContext, endEvent, fail, fromHeaders). client: compatible EventFlow client, options?: EventFlowMiddlewareOptions

EventFlowMiddlewareOptions:

Option Type Default Description
eventName string or (req) => string http:${method} ${url} Event name for non-propagated requests.
mapContext (req) => EventContext undefined Adds custom request-derived context.
includeRequestContext boolean true Adds method and url context automatically.
failOn5xx boolean true Marks event as failed when response status is >= 500.
autoEnd boolean true Ends events automatically on finish/close.

Propagation Constants

Constant Value
TRACE_ID_HEADER "x-eventflow-trace-id"
EVENT_ID_HEADER "x-eventflow-event-id"
CONTEXT_HEADER "x-eventflow-context"
ENCRYPTED_CONTEXT_HEADER "x-eventflow-encrypted-context"
EVENT_HEADER "x-eventflow-event"
EVENTFLOW_TRACE_ID_KEY "eventflow_trace_id"
EVENTFLOW_EVENT_ID_KEY "eventflow_event_id"
EVENTFLOW_EVENT_NAME_KEY "eventflow_event_name"
EVENTFLOW_PARENT_ID_KEY "eventflow_parent_id"
EVENTFLOW_CONTEXT_KEY "eventflow_context"
EVENTFLOW_ENCRYPTED_CONTEXT_KEY "eventflow_encrypted_context"

Exported Types

EventStatus, EventContext, Step, CallerInfo, EventError, EventLog, EventEmissionMode, TransportEmissionOptions, EventFlowClientConfig, EventFlowClientConfigureOptions, EventFlowClientConfigureWithUserContext, UserContextMapper, SerializedPropagationEvent, ContextManager, HeadersLike, RunCallback, RunOptions, InstrumentCallback, InstrumentedFunction, InstrumentOptions, PropagationMetadata, PropagationMetadataInput, PropagationMetadataOptions, EventFlowMiddleware, EventFlowMiddlewareOptions, NodeLikeRequest, NodeLikeResponse, NextFunction.

Contributors