Email SDK
PluginsBuilt-in

Capture plugin

Record every send, retry, response, and error in tests — and assert on them through a typed client.capture store.

capturePlugin records send lifecycle events into a store and exposes it as client.capture — a typed property added through the plugin extension system. Pair it with the memory adapter to assert exactly what the SDK tried to do, without a network call.

email.test.ts
import { createEmailClient } from "@opencoredev/email-sdk";
import { capturePlugin } from "@opencoredev/email-sdk/plugins/capture";
import { memoryProvider } from "@opencoredev/email-sdk/testing";

const email = createEmailClient({
  adapters: [memoryProvider()],
  plugins: [capturePlugin()],
});

await email.send({
  from: "Acme <hello@acme.com>",
  to: "user@example.com",
  subject: "Welcome",
  text: "Your account is ready.",
});

const sent = email.capture.events.find((event) => event.type === "afterSend");
expect(sent?.provider).toBe("memory");
expect(email.capture.events.map((event) => event.type)).toEqual(["beforeSend", "afterSend"]);

email.capture is fully typed — capturePlugin() is an EmailPlugin<{ capture: EmailCaptureStore }>, so the extension flows into the client type with no casts.

What gets captured

Event typeFires whenExtra fields
beforeSendThe message enters the pipeline (once per send).message, send-option metadata
afterSendA provider returns a normalized response.provider, attempt, response
retryThe SDK schedules another attempt on the same adapter.provider, attempt, nextAttempt, delayMs, error
errorAn adapter route fails after exhausting its retries.provider, attempt, error

Unlike the observability plugin, captured events hold the full, unredacted message — they are meant for test assertions, not production logs.

Reset between tests with email.capture.clear().

Options

Prop

Type

capturePlugin(store) is shorthand for capturePlugin({ store }).

Custom client key

clientKey renames the client property, and the type follows the literal you pass:

const email = createEmailClient({
  adapters: [memoryProvider()],
  plugins: [
    capturePlugin({ id: "capture:primary", clientKey: "primaryCapture" }),
    capturePlugin({ id: "capture:audit", clientKey: "auditCapture" }),
  ],
});

email.primaryCapture.events; // typed EmailCaptureStore
email.auditCapture.clear();

Multiple instances need distinct ids (duplicate plugin ids throw) and distinct clientKeys (a second plugin extending the client with an existing key throws).

Share a store

createEmailCaptureStore() creates a store you can hold a reference to directly — useful when the client is built inside a factory and your test only sees the store:

import { capturePlugin, createEmailCaptureStore } from "@opencoredev/email-sdk/plugins/capture";

const store = createEmailCaptureStore();
const email = buildTestClient({ plugins: [capturePlugin(store)] });

await email.send(message);
expect(store.events).toHaveLength(2);

Capture vs memory adapter

They answer different questions: memoryProvider() replaces the network so nothing real sends, while capturePlugin() records what the pipeline did — including retries and errors the adapter alone cannot show. Most tests use both. See Test email behavior for the full testing setup.

On this page