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.
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 type | Fires when | Extra fields |
|---|---|---|
beforeSend | The message enters the pipeline (once per send). | message, send-option metadata |
afterSend | A provider returns a normalized response. | provider, attempt, response |
retry | The SDK schedules another attempt on the same adapter. | provider, attempt, nextAttempt, delayMs, error |
error | An 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.
