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.
- 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
npm install eventflowjsRun a native / web version to try it out here:
- Live Snack: Checkout Orchestration: frontend -> API -> frontend continuation token flow plus metadata-based webhook continuation with live event panels.
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();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.
// 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(),
},
);// 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 });
});// 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();// 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 });
}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": ...
}
...
}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.
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.
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(defaulttrue): callEventFlow.fail(error)before rethrowing.startIfMissing(defaultfalse): auto-start an event if none exists.eventName: event name used whenstartIfMissingstarts a new event.endIfStarted(defaulttrue): auto-end only the event started by this run.statusOnAutoEnd(default"success"): status used when auto-ending.
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
runoptions (failEventOnError,startIfMissing,eventName,endIfStarted,statusOnAutoEnd) stepName: override the step recorded for each callcontextFromArgs(...args): add context derived from input argscontextFromResult(result, ...args): add context from the returned value
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";
| 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). |
| 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 |
| 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> |
| 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. |
| 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. |
| 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. |
| 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 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. |
| 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. |
| 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" |
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.
