This commit is contained in:
Alex Holovach
2025-10-05 11:02:47 -05:00
parent e93f206e89
commit 9d3de603ae
4 changed files with 169 additions and 545 deletions

View File

@@ -35,10 +35,7 @@ needed. Every SDK call creates a client span with useful attributes.
## What Gets Traced
- All top-level Resend client methods (e.g. `resend.ping`)
- Nested resource methods such as `resend.emails.send`, `resend.emails.batch`,
`resend.domains.create`, `resend.apiKeys.create`, and custom resources
- Both async and sync methods defined on resource instances or their prototypes
This instrumentation specifically wraps the `resend.emails.send` method (and its alias `resend.emails.create`), creating a single clean span for each email send operation.
## Span Attributes
@@ -47,35 +44,32 @@ Each span includes:
| Attribute | Description | Example |
| --- | --- | --- |
| `messaging.system` | Constant value `resend` | `resend` |
| `messaging.operation` | Operation derived from the method name | `send`, `create`, `list` |
| `resend.resource` | Top-level resource name | `emails`, `domains` |
| `resend.target` | Fully-qualified target (resource + method) | `emails.send` |
| `resend.recipient_count` | Total recipients detected in the request payload | `3` |
| `resend.template_id` | Template referenced in the request (when present) | `tmpl_123` |
| `resend.message_id` | Message ID returned by email operations | `email_123` |
| `resend.message_count` | How many message IDs were returned | `2` |
| `resend.resource_id` | Identifier returned by non-email resources | `domain_456` |
| `messaging.operation` | Operation type | `send` |
| `resend.resource` | Resource name | `emails` |
| `resend.target` | Full operation target | `emails.send` |
| `resend.to_addresses` | Comma-separated TO addresses | `user@example.com, another@example.com` |
| `resend.cc_addresses` | Comma-separated CC addresses (if present) | `cc@example.com` |
| `resend.bcc_addresses` | Comma-separated BCC addresses (if present) | `bcc@example.com` |
| `resend.recipient_count` | Total number of recipients | `3` |
| `resend.from` | Sender email address | `noreply@example.com` |
| `resend.subject` | Email subject | `Welcome to our service` |
| `resend.template_id` | Template ID (if using templates) | `tmpl_123` |
| `resend.message_id` | Message ID returned by Resend | `email_123` |
| `resend.message_count` | Number of messages sent (always 1 for single sends) | `1` |
Sensitive request payloads are never recorded—only counts and identifiers that
Resend already exposes.
The instrumentation captures email addresses and metadata to help with debugging and monitoring, while avoiding sensitive email content.
## Configuration
```ts
instrumentResend(resend, {
tracerName: "my-service",
captureRequestMetadata: true,
captureResponseMetadata: true,
shouldInstrument: (path, method) => !(path[0] === "emails" && method === "list"),
tracer: myCustomTracer, // optional: bring your own tracer
});
```
- `tracerName` / `tracer`: reuse an existing tracer if you have one.
- `captureRequestMetadata`: toggle attributes derived from the request payload
(recipient counts, template IDs). Enabled by default.
- `captureResponseMetadata`: toggle attributes derived from the response
(message IDs, resource IDs). Enabled by default.
- `shouldInstrument`: skip specific methods programmatically.
- `tracerName`: Custom name for the tracer (defaults to `@kubiks/otel-resend`)
- `tracer`: Use an existing tracer instance instead of creating a new one
## License

View File

@@ -1,6 +1,6 @@
{
"name": "@kubiks/otel-resend",
"version": "1.0.2",
"version": "1.0.3",
"private": false,
"publishConfig": {
"access": "public"

View File

@@ -5,6 +5,7 @@ import {
InMemorySpanExporter,
SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import type { Resend } from "resend";
import {
instrumentResend,
SEMATTRS_MESSAGING_OPERATION,
@@ -13,7 +14,6 @@ import {
SEMATTRS_RESEND_MESSAGE_ID,
SEMATTRS_RESEND_RECIPIENT_COUNT,
SEMATTRS_RESEND_RESOURCE,
SEMATTRS_RESEND_RESOURCE_ID,
SEMATTRS_RESEND_TARGET,
SEMATTRS_RESEND_TEMPLATE_ID,
SEMATTRS_RESEND_TO_ADDRESSES,
@@ -41,48 +41,37 @@ describe("instrumentResend", () => {
trace.disable();
});
const createMockResend = () => {
class EmailsResource {
async send(payload: Record<string, unknown>) {
return { id: "email_123", payload };
}
async list() {
return { data: [{ id: "email_1" }, { id: "email_2" }] };
}
fail() {
throw new Error("boom");
}
}
class DomainsResource {
async create() {
return { id: "domain_123" };
}
}
return {
emails: new EmailsResource(),
domains: new DomainsResource(),
apiKeys: {
create: vi.fn(async () => ({ id: "key_abc" })),
const createMockResend = (): Resend => {
const mockResend = {
emails: {
send: vi.fn(async (payload: any) => ({
data: { id: "email_123" },
error: null
})),
create: vi.fn(async (payload: any) => ({
data: { id: "email_123" },
error: null
})),
},
ping: vi.fn(() => "pong"),
};
} as unknown as Resend;
return mockResend;
};
it("wraps methods and records spans", async () => {
it("wraps emails.send and records spans", async () => {
const resend = createMockResend();
instrumentResend(resend);
const payload = {
to: ["user@example.com", "second@example.com"],
from: "sender@example.com",
subject: "Test Email",
text: "Hello",
template_id: "tmpl_123",
};
const response = await resend.emails.send(payload);
expect(response.id).toBe("email_123");
expect(response.data?.id).toBe("email_123");
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
@@ -100,56 +89,22 @@ describe("instrumentResend", () => {
expect(span.attributes[SEMATTRS_RESEND_MESSAGE_ID]).toBe("email_123");
expect(span.attributes[SEMATTRS_RESEND_MESSAGE_COUNT]).toBe(1);
expect(span.attributes[SEMATTRS_RESEND_RECIPIENT_COUNT]).toBe(2);
expect(span.attributes[SEMATTRS_RESEND_TEMPLATE_ID]).toBe("tmpl_123");
expect(span.attributes[SEMATTRS_RESEND_TO_ADDRESSES]).toBe("user@example.com, second@example.com");
expect(span.status.code).toBe(SpanStatusCode.OK);
});
it("records spans for prototype methods", async () => {
const resend = createMockResend();
instrumentResend(resend);
await resend.domains.create();
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
const span = spans[0];
if (!span) {
throw new Error("Expected a span to be recorded");
}
expect(span.name).toBe("resend.domains.create");
expect(span.attributes[SEMATTRS_RESEND_RESOURCE]).toBe("domains");
expect(span.attributes[SEMATTRS_RESEND_MESSAGE_ID]).toBeUndefined();
expect(span.attributes[SEMATTRS_RESEND_RESOURCE_ID]).toBe("domain_123");
expect(span.status.code).toBe(SpanStatusCode.OK);
});
it("handles synchronous functions", () => {
const resend = createMockResend();
instrumentResend(resend);
const result = resend.ping();
expect(result).toBe("pong");
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
const span = spans[0];
if (!span) {
throw new Error("Expected a span to be recorded");
}
expect(span.name).toBe("resend.ping");
expect(span.attributes[SEMATTRS_RESEND_TEMPLATE_ID]).toBe("tmpl_123");
expect(span.attributes[SEMATTRS_RESEND_FROM]).toBe("sender@example.com");
expect(span.attributes[SEMATTRS_RESEND_SUBJECT]).toBe("Test Email");
expect(span.status.code).toBe(SpanStatusCode.OK);
});
it("captures errors and marks span status", async () => {
const resend = createMockResend();
resend.emails.send = vi.fn().mockRejectedValue(new Error("boom"));
instrumentResend(resend);
await expect(async () => resend.emails.fail()).rejects.toThrowError("boom");
await expect(async () =>
resend.emails.send({ to: "test@example.com", from: "sender@example.com", subject: "Test", text: "Test" })
).rejects.toThrowError("boom");
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
@@ -170,28 +125,15 @@ describe("instrumentResend", () => {
const second = instrumentResend(first);
expect(first).toBe(second);
expect(first.emails.send).toBe(second.emails.send);
await second.emails.send({});
expect(exporter.getFinishedSpans()).toHaveLength(1);
});
it("respects shouldInstrument filter", async () => {
const resend = createMockResend();
instrumentResend(resend, {
shouldInstrument: (path, methodName) => {
if (path[0] === "emails" && methodName === "list") {
return false;
}
return true;
},
await second.emails.send({
to: "test@example.com",
from: "sender@example.com",
subject: "Test",
text: "Test"
});
await resend.emails.list();
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(0);
expect(exporter.getFinishedSpans()).toHaveLength(1);
});
it("captures email addresses from all recipient fields", async () => {
@@ -231,6 +173,9 @@ describe("instrumentResend", () => {
const payload = {
to: "single@example.com",
from: "sender@example.com",
subject: "Test",
text: "Test",
};
await resend.emails.send(payload);
@@ -279,4 +224,31 @@ describe("instrumentResend", () => {
expect(span.attributes[SEMATTRS_RESEND_FROM]).toBe("noreply@example.com");
expect(span.attributes[SEMATTRS_RESEND_SUBJECT]).toBe("Mixed Format Test");
});
});
it("also instruments emails.create as an alias", async () => {
const resend = createMockResend();
instrumentResend(resend);
const payload = {
to: "user@example.com",
from: "sender@example.com",
subject: "Test",
text: "Test",
};
// Use create instead of send
await resend.emails.create(payload);
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
const span = spans[0];
if (!span) {
throw new Error("Expected a span to be recorded");
}
// Should still show as emails.send since create is just an alias
expect(span.name).toBe("resend.emails.send");
expect(span.attributes[SEMATTRS_RESEND_TARGET]).toBe("emails.send");
});
});

View File

@@ -6,12 +6,12 @@ import {
type Span,
type Tracer,
} from "@opentelemetry/api";
import type { CreateEmailOptions } from "resend";
import type { Resend, CreateEmailOptions, CreateEmailResponse } from "resend";
const DEFAULT_TRACER_NAME = "@kubiks/otel-resend";
const INSTRUMENTED_FLAG = "__kubiksOtelResendInstrumented" as const;
const INSTRUMENTED_METHOD_FLAG = Symbol("kubiksOtelResendInstrumentedMethod");
const INSTRUMENTED_FLAG = Symbol("kubiksOtelResendInstrumented");
// Semantic attribute constants
export const SEMATTRS_MESSAGING_SYSTEM = "messaging.system" as const;
export const SEMATTRS_MESSAGING_OPERATION = "messaging.operation" as const;
export const SEMATTRS_RESEND_RESOURCE = "resend.resource" as const;
@@ -32,79 +32,12 @@ export const SEMATTRS_RESEND_SUBJECT = "resend.subject" as const;
export interface InstrumentResendConfig {
tracerName?: string;
tracer?: Tracer;
captureRequestMetadata?: boolean;
captureResponseMetadata?: boolean;
shouldInstrument?(
path: readonly string[],
methodName: string,
original: AnyFunction,
): boolean;
}
type AnyFunction = (...args: unknown[]) => unknown;
type ResendLike = Record<string, unknown>;
interface InstrumentedResendLike extends ResendLike {
interface InstrumentedResend extends Resend {
[INSTRUMENTED_FLAG]?: true;
}
interface NormalizedConfig {
tracer: Tracer;
tracerName: string;
captureRequestMetadata: boolean;
captureResponseMetadata: boolean;
shouldInstrument(
path: readonly string[],
methodName: string,
original: AnyFunction,
): boolean;
}
const instrumentedObjects = new WeakSet<object>();
const defaultShouldInstrument: NormalizedConfig["shouldInstrument"] = () => true;
function toSnakeCase(input: string): string {
return input
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.replace(/[\s./-]+/g, "_")
.toLowerCase();
}
function buildSpanName(path: readonly string[], methodName: string): string {
const parts = [...path, methodName].filter(Boolean);
return parts.length ? `resend.${parts.join(".")}` : "resend.call";
}
function buildBaseAttributes(
path: readonly string[],
methodName: string,
): Record<string, string> {
const attributes: Record<string, string> = {
[SEMATTRS_MESSAGING_SYSTEM]: "resend",
[SEMATTRS_MESSAGING_OPERATION]: toSnakeCase(methodName),
[SEMATTRS_RESEND_TARGET]: [...path, methodName].join("."),
};
if (path[0]) {
attributes[SEMATTRS_RESEND_RESOURCE] = path[0];
}
return attributes;
}
function countRecipients(value: string | string[] | undefined): number {
if (!value) {
return 0;
}
if (typeof value === "string") {
return value.trim() ? 1 : 0;
}
if (Array.isArray(value)) {
return value.filter(email => typeof email === "string" && email.trim()).length;
}
return 0;
}
function extractEmailAddresses(value: string | string[] | undefined): string[] {
if (!value) {
return [];
@@ -119,164 +52,57 @@ function extractEmailAddresses(value: string | string[] | undefined): string[] {
return [];
}
function annotateRequest(
span: Span,
path: readonly string[],
args: unknown[],
capture: boolean,
): void {
if (!capture || !args.length) {
return;
function annotateEmailSpan(span: Span, payload: CreateEmailOptions): void {
// Set base attributes
span.setAttributes({
[SEMATTRS_MESSAGING_SYSTEM]: "resend",
[SEMATTRS_MESSAGING_OPERATION]: "send",
[SEMATTRS_RESEND_RESOURCE]: "emails",
[SEMATTRS_RESEND_TARGET]: "emails.send",
});
// Extract and set email addresses
const toAddresses = extractEmailAddresses(payload.to);
if (toAddresses.length > 0) {
span.setAttribute(SEMATTRS_RESEND_TO_ADDRESSES, toAddresses.join(", "));
}
const payload = args[0];
if (!payload || typeof payload !== "object") {
return;
const ccAddresses = extractEmailAddresses(payload.cc);
if (ccAddresses.length > 0) {
span.setAttribute(SEMATTRS_RESEND_CC_ADDRESSES, ccAddresses.join(", "));
}
// Check if this is an email send operation
if (path[0] === "emails" || path[0] === "batch") {
// Try to cast to CreateEmailOptions for better type safety
const data = payload as Partial<CreateEmailOptions>;
// Extract and set email addresses using proper types
const toAddresses = extractEmailAddresses(data.to);
if (toAddresses.length > 0) {
span.setAttribute(SEMATTRS_RESEND_TO_ADDRESSES, toAddresses.join(", "));
}
const ccAddresses = extractEmailAddresses(data.cc);
if (ccAddresses.length > 0) {
span.setAttribute(SEMATTRS_RESEND_CC_ADDRESSES, ccAddresses.join(", "));
}
const bccAddresses = extractEmailAddresses(data.bcc);
if (bccAddresses.length > 0) {
span.setAttribute(SEMATTRS_RESEND_BCC_ADDRESSES, bccAddresses.join(", "));
}
// Count recipients
const recipientCount = toAddresses.length + ccAddresses.length + bccAddresses.length;
if (recipientCount > 0) {
span.setAttribute(SEMATTRS_RESEND_RECIPIENT_COUNT, recipientCount);
}
// Handle other email-specific attributes
if (data.subject) {
span.setAttribute(SEMATTRS_RESEND_SUBJECT, data.subject);
}
if (data.from) {
span.setAttribute(SEMATTRS_RESEND_FROM, data.from);
}
} else {
// For non-email operations, use generic handling
const data = payload as Record<string, unknown>;
// Only count if the fields exist and are the right type
let recipientCount = 0;
if (typeof data.to === "string" || Array.isArray(data.to)) {
recipientCount += countRecipients(data.to as string | string[]);
}
if (typeof data.cc === "string" || Array.isArray(data.cc)) {
recipientCount += countRecipients(data.cc as string | string[]);
}
if (typeof data.bcc === "string" || Array.isArray(data.bcc)) {
recipientCount += countRecipients(data.bcc as string | string[]);
}
if (recipientCount > 0) {
span.setAttribute(SEMATTRS_RESEND_RECIPIENT_COUNT, recipientCount);
}
const bccAddresses = extractEmailAddresses(payload.bcc);
if (bccAddresses.length > 0) {
span.setAttribute(SEMATTRS_RESEND_BCC_ADDRESSES, bccAddresses.join(", "));
}
// Handle generic attributes that apply to all operations
const data = payload as Record<string, unknown>;
const templateId =
(typeof data.template_id === "string" && data.template_id) ||
(typeof data.templateId === "string" && data.templateId) ||
(typeof data.template === "string" && data.template);
if (templateId) {
// Count recipients
const recipientCount = toAddresses.length + ccAddresses.length + bccAddresses.length;
if (recipientCount > 0) {
span.setAttribute(SEMATTRS_RESEND_RECIPIENT_COUNT, recipientCount);
}
// Set other email attributes
if (payload.subject) {
span.setAttribute(SEMATTRS_RESEND_SUBJECT, payload.subject);
}
if (payload.from) {
span.setAttribute(SEMATTRS_RESEND_FROM, payload.from);
}
// Handle template IDs (support both formats for compatibility)
const templateId = (payload as any).template_id || (payload as any).templateId || (payload as any).template;
if (templateId && typeof templateId === "string") {
span.setAttribute(SEMATTRS_RESEND_TEMPLATE_ID, templateId);
}
const segmentId =
(typeof data.segment_id === "string" && data.segment_id) ||
(typeof data.segmentId === "string" && data.segmentId);
if (segmentId) {
span.setAttribute(SEMATTRS_RESEND_SEGMENT_ID, segmentId);
}
const audienceId =
(typeof data.audience_id === "string" && data.audience_id) ||
(typeof data.audienceId === "string" && data.audienceId);
if (audienceId) {
span.setAttribute(SEMATTRS_RESEND_AUDIENCE_ID, audienceId);
}
}
function collectIdentifiers(value: unknown, depth = 0): string[] {
if (!value || depth > 3) {
return [];
}
if (typeof value === "string") {
return value ? [value] : [];
}
if (Array.isArray(value)) {
return value.flatMap((item) => collectIdentifiers(item, depth + 1));
}
if (typeof value === "object") {
const record = value as Record<string, unknown>;
const ids: string[] = [];
const directId =
(typeof record.id === "string" && record.id) ||
(typeof record.messageId === "string" && record.messageId) ||
(typeof record.message_id === "string" && record.message_id);
if (directId) {
ids.push(directId);
}
const nestedKeys = ["data", "items", "messages", "results", "entries"];
for (const key of nestedKeys) {
if (key in record) {
ids.push(...collectIdentifiers(record[key], depth + 1));
}
}
return ids;
}
return [];
}
function annotateResponse(
span: Span,
resource: string | undefined,
result: unknown,
capture: boolean,
): void {
if (!capture) {
return;
}
const identifiers = collectIdentifiers(result);
if (!identifiers.length) {
return;
}
const uniqueIds = Array.from(new Set(identifiers));
span.setAttribute(SEMATTRS_RESEND_MESSAGE_COUNT, uniqueIds.length);
if (resource === "emails") {
if (uniqueIds.length === 1) {
span.setAttribute(SEMATTRS_RESEND_MESSAGE_ID, uniqueIds[0]!);
}
} else if (uniqueIds.length === 1) {
span.setAttribute(SEMATTRS_RESEND_RESOURCE_ID, uniqueIds[0]!);
function annotateEmailResponse(span: Span, response: CreateEmailResponse): void {
if (response.data?.id) {
span.setAttribute(SEMATTRS_RESEND_MESSAGE_ID, response.data.id);
span.setAttribute(SEMATTRS_RESEND_MESSAGE_COUNT, 1);
}
}
@@ -294,223 +120,55 @@ function finalizeSpan(span: Span, error?: unknown): void {
span.end();
}
function wrapMethod(
original: AnyFunction,
path: readonly string[],
methodName: string,
tracer: Tracer,
config: NormalizedConfig,
): AnyFunction {
const spanName = buildSpanName(path, methodName);
const baseAttributes = buildBaseAttributes(path, methodName);
const resource = path[0];
const instrumented = function instrumentedResendMethod(
this: unknown,
...args: unknown[]
) {
const span = tracer.startSpan(spanName, {
kind: SpanKind.CLIENT,
attributes: baseAttributes,
});
annotateRequest(span, path, args, config.captureRequestMetadata);
const activeContext = trace.setSpan(context.active(), span);
const invokeOriginal = () => original.apply(this, args);
try {
const result = context.with(activeContext, invokeOriginal);
if (result && typeof (result as Promise<unknown>).then === "function") {
return (result as Promise<unknown>)
.then((value) => {
annotateResponse(span, resource, value, config.captureResponseMetadata);
finalizeSpan(span);
return value;
})
.catch((error: unknown) => {
finalizeSpan(span, error);
throw error;
});
}
annotateResponse(span, resource, result, config.captureResponseMetadata);
finalizeSpan(span);
return result;
} catch (error) {
finalizeSpan(span, error);
throw error;
}
};
(instrumented as { [INSTRUMENTED_METHOD_FLAG]?: true })[INSTRUMENTED_METHOD_FLAG] = true;
return instrumented;
}
function instrumentObject(
target: ResendLike,
path: readonly string[],
tracer: Tracer,
config: NormalizedConfig,
): void {
if (!target || typeof target !== "object") {
return;
}
if (instrumentedObjects.has(target)) {
return;
}
instrumentedObjects.add(target);
const processedKeys = new Set<string>();
for (const key of Reflect.ownKeys(target)) {
if (typeof key === "symbol") {
continue;
}
if (key === INSTRUMENTED_FLAG) {
continue;
}
processedKeys.add(key);
const descriptor = Object.getOwnPropertyDescriptor(target, key);
let value: unknown;
if (!descriptor || "value" in descriptor) {
value = (target as ResendLike)[key];
}
if (typeof value === "function") {
const original = value as AnyFunction;
if ((original as { [INSTRUMENTED_METHOD_FLAG]?: true })[INSTRUMENTED_METHOD_FLAG]) {
continue;
}
if (!config.shouldInstrument(path, key, original)) {
continue;
}
const wrapped = wrapMethod(original, path, key, tracer, config);
let replaced = false;
try {
replaced = Reflect.set(target, key, wrapped);
} catch {
replaced = false;
}
if (!replaced) {
Object.defineProperty(target, key, {
configurable: descriptor?.configurable ?? true,
enumerable: descriptor?.enumerable ?? true,
writable: descriptor?.writable ?? true,
value: wrapped,
});
}
continue;
}
if (value && typeof value === "object") {
instrumentObject(value as ResendLike, [...path, key], tracer, config);
continue;
}
if (descriptor && (descriptor.get || descriptor.set)) {
try {
const resolved = (target as ResendLike)[key];
if (resolved && typeof resolved === "object") {
instrumentObject(resolved as ResendLike, [...path, key], tracer, config);
}
} catch {
// Ignore accessor errors.
}
}
}
let prototype = Object.getPrototypeOf(target);
while (
prototype &&
prototype !== Object.prototype &&
prototype !== Function.prototype
) {
for (const key of Reflect.ownKeys(prototype)) {
if (typeof key === "symbol" || key === "constructor") {
continue;
}
if (processedKeys.has(key)) {
continue;
}
const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
if (!descriptor || typeof descriptor.value !== "function") {
continue;
}
const original = descriptor.value as AnyFunction;
if ((original as { [INSTRUMENTED_METHOD_FLAG]?: true })[INSTRUMENTED_METHOD_FLAG]) {
continue;
}
if (!config.shouldInstrument(path, key, original)) {
continue;
}
const wrapped = wrapMethod(original, path, key, tracer, config);
let replaced = false;
try {
replaced = Reflect.set(target, key, wrapped);
} catch {
replaced = false;
}
if (!replaced) {
Object.defineProperty(target, key, {
configurable: true,
enumerable: descriptor.enumerable ?? true,
writable: true,
value: wrapped,
});
}
processedKeys.add(key);
}
prototype = Object.getPrototypeOf(prototype);
}
}
export function instrumentResend<TClient extends ResendLike>(
client: TClient,
config?: InstrumentResendConfig,
): TClient {
if (!client || typeof client !== "object") {
return client;
}
if ((client as InstrumentedResendLike)[INSTRUMENTED_FLAG]) {
export function instrumentResend(client: Resend, config?: InstrumentResendConfig): Resend {
// Check if already instrumented
if ((client as InstrumentedResend)[INSTRUMENTED_FLAG]) {
return client;
}
const tracerName = config?.tracerName ?? DEFAULT_TRACER_NAME;
const tracer = config?.tracer ?? trace.getTracer(tracerName);
const normalizedConfig: NormalizedConfig = {
tracer,
tracerName,
captureRequestMetadata: config?.captureRequestMetadata ?? true,
captureResponseMetadata: config?.captureResponseMetadata ?? true,
shouldInstrument: config?.shouldInstrument ?? defaultShouldInstrument,
// Save the original send method
const originalSend = client.emails.send.bind(client.emails);
// Replace the send method with our instrumented version
client.emails.send = async function instrumentedSend(
payload: CreateEmailOptions
): Promise<CreateEmailResponse> {
const span = tracer.startSpan("resend.emails.send", {
kind: SpanKind.CLIENT,
});
// Annotate span with email details
annotateEmailSpan(span, payload);
// Set the span as active
const activeContext = trace.setSpan(context.active(), span);
try {
// Call the original method within the active context
const response = await context.with(activeContext, () => originalSend(payload));
// Annotate with response data
annotateEmailResponse(span, response);
// Mark as successful
finalizeSpan(span);
return response;
} catch (error) {
// Mark as failed
finalizeSpan(span, error);
throw error;
}
};
instrumentObject(client, [], tracer, normalizedConfig);
// Also wrap the create method (it's an alias for send)
client.emails.create = client.emails.send;
(client as InstrumentedResendLike)[INSTRUMENTED_FLAG] = true;
// Mark as instrumented
(client as InstrumentedResend)[INSTRUMENTED_FLAG] = true;
return client;
}
}