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. This is 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.
