Skip to main content

Overview

@kubiks/otel-resend provides OpenTelemetry instrumentation for the Resend email service Node.js SDK. Capture spans for every email operation with detailed metadata about recipients, subjects, and delivery status.
Resend Trace Visualization
Visualize your email operations with detailed span information including recipients, subject lines, and delivery status—without capturing sensitive email content.

Installation

npm install @kubiks/otel-resend
Peer Dependencies: @opentelemetry/api >= 1.9.0, resend >= 3.0.0

Quick Start

import { Resend } from "resend";
import { instrumentResend } from "@kubiks/otel-resend";

const resend = instrumentResend(new Resend(process.env.RESEND_API_KEY!));

await resend.emails.send({
  from: "hello@example.com",
  to: ["user@example.com"],
  subject: "Welcome",
  html: "<p>Hello world</p>",
});
instrumentResend wraps the instance you already use—no configuration changes needed. Every SDK call creates a client span with useful attributes.

What Gets Traced

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.
Only metadata is captured—email content (HTML, text, attachments) is never included in traces for privacy and security.

Span Attributes

Each span includes rich metadata about the email operation:
AttributeDescriptionExample
messaging.systemConstant value resendresend
messaging.operationOperation typesend
resend.resourceResource nameemails
resend.targetFull operation targetemails.send
resend.to_addressesComma-separated TO addressesuser@example.com, another@example.com
resend.cc_addressesComma-separated CC addresses (if present)cc@example.com
resend.bcc_addressesComma-separated BCC addresses (if present)bcc@example.com
resend.recipient_countTotal number of recipients3
resend.fromSender email addressnoreply@example.com
resend.subjectEmail subjectWelcome to our service
resend.template_idTemplate ID (if using templates)tmpl_123
resend.message_idMessage ID returned by Resendemail_123
resend.message_countNumber of messages sent (always 1 for single sends)1
The instrumentation captures email addresses and metadata to help with debugging and monitoring, while avoiding sensitive email content.

Usage Examples

Basic Email

import { resend } from "@/lib/resend";

await resend.emails.send({
  from: "noreply@example.com",
  to: "user@example.com",
  subject: "Welcome to our platform",
  html: "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
});

// Traced with:
// - resend.from: "noreply@example.com"
// - resend.to_addresses: "user@example.com"
// - resend.subject: "Welcome to our platform"
// - resend.recipient_count: 1

With CC and BCC

import { resend } from "@/lib/resend";

await resend.emails.send({
  from: "sales@example.com",
  to: "customer@example.com",
  cc: ["manager@example.com", "team@example.com"],
  bcc: "archive@example.com",
  subject: "Project Proposal",
  html: "<p>Please find the proposal attached.</p>",
});

// Traced with:
// - resend.to_addresses: "customer@example.com"
// - resend.cc_addresses: "manager@example.com, team@example.com"
// - resend.bcc_addresses: "archive@example.com"
// - resend.recipient_count: 4
BCC addresses are included in the span but remain hidden from other recipients as expected.

Using Email Templates

import { resend } from "@/lib/resend";
import { WelcomeEmail } from "@/emails/welcome";

await resend.emails.send({
  from: "onboarding@example.com",
  to: "user@example.com",
  subject: "Welcome aboard!",
  react: WelcomeEmail({ name: "John" }),
});

With Attachments

import { resend } from "@/lib/resend";
import fs from "fs";

await resend.emails.send({
  from: "documents@example.com",
  to: "user@example.com",
  subject: "Your Invoice",
  html: "<p>Please find your invoice attached.</p>",
  attachments: [
    {
      filename: "invoice.pdf",
      content: fs.readFileSync("./invoice.pdf"),
    },
  ],
});

// Note: Attachment content is NOT captured in traces

Transactional Emails

import { resend } from "@/lib/resend";

export async function sendPasswordResetEmail(email: string, token: string) {
  await resend.emails.send({
    from: "security@example.com",
    to: email,
    subject: "Reset your password",
    html: `
      <h1>Password Reset Request</h1>
      <p>Click the link below to reset your password:</p>
      <a href="https://example.com/reset?token=${token}">Reset Password</a>
      <p>This link expires in 1 hour.</p>
    `,
  });
}

Complete Integration Example

Here’s a complete example of Resend with OpenTelemetry in a Next.js application:
lib/resend.ts
import { Resend } from "resend";
import { instrumentResend } from "@kubiks/otel-resend";

export const resend = instrumentResend(
  new Resend(process.env.RESEND_API_KEY!)
);
lib/email.ts
import { resend } from "@/lib/resend";

export async function sendWelcomeEmail(email: string, name: string) {
  try {
    const { data, error } = await resend.emails.send({
      from: "onboarding@example.com",
      to: email,
      subject: `Welcome ${name}!`,
      html: `
        <h1>Welcome to our platform, ${name}!</h1>
        <p>We're excited to have you on board.</p>
      `,
    });

    if (error) {
      console.error("Failed to send welcome email:", error);
      return { success: false, error };
    }

    return { success: true, messageId: data?.id };
  } catch (error) {
    console.error("Error sending email:", error);
    return { success: false, error };
  }
}

export async function sendNotification(
  email: string,
  subject: string,
  message: string
) {
  const { data, error } = await resend.emails.send({
    from: "notifications@example.com",
    to: email,
    subject,
    html: `<p>${message}</p>`,
  });

  return { data, error };
}
app/api/auth/signup/route.ts
import { NextRequest, NextResponse } from "next/server";
import { sendWelcomeEmail } from "@/lib/email";

export async function POST(request: NextRequest) {
  const { email, name } = await request.json();

  // Create user...
  
  // Send welcome email (automatically traced)
  const result = await sendWelcomeEmail(email, name);

  if (!result.success) {
    return NextResponse.json(
      { error: "Failed to send welcome email" },
      { status: 500 }
    );
  }

  return NextResponse.json({ 
    success: true,
    messageId: result.messageId 
  });
}
app/actions/email.ts
"use server";

import { resend } from "@/lib/resend";

export async function sendContactFormEmail(
  name: string,
  email: string,
  message: string
) {
  const { data, error } = await resend.emails.send({
    from: "contact@example.com",
    to: "support@example.com",
    replyTo: email,
    subject: `Contact Form: ${name}`,
    html: `
      <h2>New Contact Form Submission</h2>
      <p><strong>Name:</strong> ${name}</p>
      <p><strong>Email:</strong> ${email}</p>
      <p><strong>Message:</strong></p>
      <p>${message}</p>
    `,
  });

  if (error) {
    return { success: false, error: error.message };
  }

  return { success: true, messageId: data?.id };
}

Best Practices

Always store API keys in environment variables:
const resend = instrumentResend(
  new Resend(process.env.RESEND_API_KEY!)
);
Never commit API keys to version control.
Always check for errors when sending emails:
const { data, error } = await resend.emails.send({
  from: "noreply@example.com",
  to: email,
  subject: "Test",
  html: "<p>Test</p>",
});

if (error) {
  console.error("Email error:", error);
  // Handle error appropriately
  return;
}

console.log("Email sent:", data?.id);
Set up domain verification in Resend for production:
// Use your verified domain
from: "noreply@yourdomain.com"

// Not: "noreply@example.com"
Be mindful of Resend rate limits and implement appropriate rate limiting:
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "1 h"), // 10 emails per hour
});

export async function sendEmail(to: string, subject: string, html: string) {
  const { success } = await ratelimit.limit(to);
  
  if (!success) {
    throw new Error("Rate limit exceeded");
  }

  return await resend.emails.send({
    from: "noreply@example.com",
    to,
    subject,
    html,
  });
}
Use React Email or Resend templates for maintainable email content:
import { WelcomeEmail } from "@/emails/welcome";

await resend.emails.send({
  from: "onboarding@example.com",
  to: email,
  subject: "Welcome!",
  react: WelcomeEmail({ name: userName }),
});

Troubleshooting

Ensure OpenTelemetry is properly configured:
import { NodeSDK } from "@opentelemetry/sdk-node";

const sdk = new NodeSDK({
  // ... configuration
});

sdk.start();
The message ID is only available after successful email sending. Check for errors:
const { data, error } = await resend.emails.send({ ... });

if (error) {
  console.error("No message ID because of error:", error);
} else {
  console.log("Message ID:", data?.id);
}
Check your Resend dashboard for delivery status. Common issues:
  • Domain not verified
  • Invalid recipient address
  • Rate limits exceeded
  • API key issues

Integration with React Email

// emails/welcome.tsx
import {
  Body,
  Container,
  Head,
  Heading,
  Html,
  Link,
  Preview,
  Text,
} from "@react-email/components";

interface WelcomeEmailProps {
  name: string;
}

export function WelcomeEmail({ name }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to our platform!</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Welcome, {name}!</Heading>
          <Text style={text}>
            Thanks for joining us. We're excited to have you on board.
          </Text>
          <Link href="https://example.com/getting-started" style={link}>
            Get Started
          </Link>
        </Container>
      </Body>
    </Html>
  );
}

const main = { backgroundColor: "#f6f9fc", fontFamily: "sans-serif" };
const container = { margin: "0 auto", padding: "20px 0 48px" };
const h1 = { fontSize: "32px", fontWeight: "bold" };
const text = { fontSize: "16px", lineHeight: "26px" };
const link = { color: "#5e6ad2", textDecoration: "underline" };

Resources

License

MIT