Skip to main content

Overview

@kubiks/otel-better-auth provides comprehensive OpenTelemetry instrumentation for Better Auth. Get complete authentication observability across all auth flows with a single line of code—OAuth, email/password, sessions, account management, and more.
Better Auth Trace Visualization
Visualize your authentication flows with detailed span information including operation type, user IDs, session IDs, auth methods, and success/failure status.

Installation

npm install @kubiks/otel-better-auth
Peer Dependencies: @opentelemetry/api >= 1.9.0, better-auth >= 1.0.0

Quick Start

import { betterAuth } from "better-auth";
import { instrumentBetterAuth } from "@kubiks/otel-better-auth";

export const auth = instrumentBetterAuth(
  betterAuth({
    database: db,
    // ... your Better Auth config
  }),
);
Instrumenting Better Auth is just a single call—wrap the instance you already create and every API method invocation is traced automatically. Keep the rest of your configuration unchanged.

Traced Operations

  • Authentication
  • Session Management
  • Account Management
  • Password & Email
Sign In & Sign Up:
  • auth.http.oauth.callback.{provider} - OAuth callback with user ID
  • auth.http.signin.email - Email signin with user ID
  • auth.http.signup.email - Email signup with user ID
  • auth.http.oauth.initiate.{provider} - OAuth initiation
  • auth.http.signout - User signout
  • auth.http.get_session - Get session

Span Attributes

Each span includes rich context about the authentication operation:
AttributeDescriptionExample
auth.operationType of operationsignin, signup, get_session, signout
auth.methodAuth methodemail, oauth
auth.providerOAuth provider (when applicable)google, github
auth.successOperation successtrue, false
auth.errorError message (when failed)Invalid credentials
user.idUser ID (when available)user_123456
user.emailUser email (when available)user@example.com
session.idSession ID (when available)session_abcdef
User IDs and session IDs are captured where applicable to help with debugging and monitoring authentication flows.

Configuration

You can optionally customize the instrumentation:
instrumentBetterAuth(authClient, {
  tracerName: "my-app", // Custom tracer name
  tracer: customTracer, // Custom tracer instance
});

Usage Examples

Basic Setup (Next.js App Router)

1

Configure Better Auth

lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { instrumentBetterAuth } from "@kubiks/otel-better-auth";
import { db } from "./db";

export const auth = instrumentBetterAuth(
  betterAuth({
    baseURL: process.env.BETTER_AUTH_URL,
    database: drizzleAdapter(db, { provider: "pg" }),
    socialProviders: {
      github: {
        clientId: process.env.GITHUB_CLIENT_ID,
        clientSecret: process.env.GITHUB_CLIENT_SECRET,
      },
      google: {
        clientId: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      },
    },
  }),
);
2

Create Auth Handler

app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";

export const { GET, POST } = auth.handler;
3

Use in Your App

All authentication operations are now automatically traced!
// Sign in
await auth.api.signInEmail({
  email: "user@example.com",
  password: "password",
});

// Get session
const session = await auth.api.getSession();

OAuth Authentication

export const auth = instrumentBetterAuth(
  betterAuth({
    database: db,
    socialProviders: {
      github: {
        clientId: process.env.GITHUB_CLIENT_ID,
        clientSecret: process.env.GITHUB_CLIENT_SECRET,
      },
    },
  }),
);

// OAuth callback is automatically traced with provider name
// Span: auth.http.oauth.callback.github

Email/Password Authentication

"use server";

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

export async function signUp(email: string, password: string, name: string) {
  const result = await auth.api.signUpEmail({
    email,
    password,
    name,
  });

  // Traced as: auth.http.signup.email
  // Includes: user.id, user.email, auth.success

  return result;
}

Session Management

"use server";

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

export async function getCurrentSession() {
  const session = await auth.api.getSession();
  
  // Traced as: auth.api.get_session
  // Includes: user.id, session.id, auth.success
  
  return session;
}

Account Management

"use server";

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

export async function updateProfile(name: string, image?: string) {
  const result = await auth.api.updateUser({
    name,
    image,
  });

  // Traced as: auth.api.update_user
  // Includes: user.id, auth.success

  return result;
}

Password Management

"use server";

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

export async function changePassword(
  currentPassword: string,
  newPassword: string
) {
  const result = await auth.api.changePassword({
    currentPassword,
    newPassword,
  });

  // Traced as: auth.api.change_password
  // Includes: user.id, auth.success

  return result;
}

Email Management

"use server";

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

export async function verifyEmail(token: string) {
  const result = await auth.api.verifyEmail({ token });

  // Traced as: auth.api.verify_email
  // Includes: user.id, user.email, auth.success

  return result;
}

Complete Integration Example

Here’s a full example of Better Auth with OpenTelemetry in a Next.js application:
lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { instrumentBetterAuth } from "@kubiks/otel-better-auth";
import { db } from "./db";

export const auth = instrumentBetterAuth(
  betterAuth({
    baseURL: process.env.BETTER_AUTH_URL!,
    database: drizzleAdapter(db, { provider: "pg" }),
    
    emailAndPassword: {
      enabled: true,
      requireEmailVerification: true,
    },
    
    socialProviders: {
      github: {
        clientId: process.env.GITHUB_CLIENT_ID!,
        clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      },
      google: {
        clientId: process.env.GOOGLE_CLIENT_ID!,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      },
    },
    
    session: {
      expiresIn: 60 * 60 * 24 * 7, // 1 week
      updateAge: 60 * 60 * 24, // 1 day
    },
  }),
  {
    tracerName: "my-app-auth",
  }
);
app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";

export const { GET, POST } = auth.handler;
app/actions/auth.ts
"use server";

import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";

export async function signInWithEmail(email: string, password: string) {
  const result = await auth.api.signInEmail({
    email,
    password,
  });

  if (!result.error) {
    redirect("/dashboard");
  }

  return result;
}

export async function signUpWithEmail(
  email: string,
  password: string,
  name: string
) {
  const result = await auth.api.signUpEmail({
    email,
    password,
    name,
  });

  if (!result.error) {
    redirect("/verify-email");
  }

  return result;
}

export async function signOut() {
  await auth.api.signOut();
  redirect("/");
}

Best Practices

Always use Server Actions for authentication operations in Next.js:
"use server";

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

export async function signIn(email: string, password: string) {
  return await auth.api.signInEmail({ email, password });
}
Check for errors and provide appropriate feedback:
const result = await auth.api.signInEmail({ email, password });

if (result.error) {
  return { error: result.error.message };
}

return { success: true };
Always enable email verification for production:
betterAuth({
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
})
Use descriptive tracer names for easier debugging:
instrumentBetterAuth(auth, {
  tracerName: "my-app-auth",
});

Troubleshooting

Ensure OpenTelemetry is properly initialized before creating the auth instance:
import { NodeSDK } from "@opentelemetry/sdk-node";

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

sdk.start();

// Then create auth instance
export const auth = instrumentBetterAuth(betterAuth({ ... }));
User IDs are only captured for operations where the user is authenticated. For sign-in and sign-up, the user ID is captured after successful authentication.
Make sure you’re using the correct provider name in your Better Auth configuration. The provider name must match exactly (e.g., github, not GitHub).

Resources

License

MIT