0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · 강의

서버 액션 실패와 원격 측정 데이터 수집

사용자 컨텍스트와 함께 액션 오류 및 성능 지표를 관측성 백엔드에 보고하는 방법을 배웁니다.

서버 액션 실패와 원격 측정 데이터 수집은(는) CoddyKit의 무료 Next.js 15 Fullstack (App Router + Server Actions) 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Next.js 15 Fullstack (App Router + Server Actions) 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Next.js 15 Fullstack (App Router + Server Actions) 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Server Action Failures Need Special Treatment

Server Actions in Next.js 15 run on the server, outside the browser's error boundary. When a Server Action throws, the client receives a generic error — no stack trace, no context, no telemetry.

This means standard try/catch in the action is necessary but not sufficient. You also need to:

  • Capture the error with full server-side context (user ID, action name, request metadata)
  • Forward structured telemetry to an observability backend (e.g. OpenTelemetry, Sentry, Datadog)
  • Return a safe, serialisable error payload to the client — never expose raw stack traces

In this lesson you will build a complete, production-grade error-and-metrics pipeline around Server Actions.

Structuring a Safe Action Result Type

Before adding telemetry, agree on the shape every Server Action returns. A discriminated union makes error handling exhaustive on the client and prevents accidental data leaks.

Define this once in a shared types file and import it everywhere:

// lib/action-result.ts
export type ActionSuccess<T> = {
  ok: true;
  data: T;
  durationMs: number;
};

export type ActionFailure = {
  ok: false;
  code: string;       // machine-readable, e.g. 'VALIDATION_ERROR'
  message: string;    // human-readable, safe to show
  durationMs: number;
};

export type ActionResult<T> = ActionSuccess<T> | ActionFailure;

// Helper constructors
export function ok<T>(data: T, durationMs: number): ActionSuccess<T> {
  return { ok: true, data, durationMs };
}

export function fail(code: string, message: string, durationMs: number): ActionFailure {
  return { ok: false, code, message, durationMs };
}

Instrumenting Duration at the Action Level

Performance metrics begin with measuring how long each action takes. Wrap every action body with a performance.now() call so duration is always available for telemetry — even when the action fails.

The pattern below ensures durationMs is captured in both success and error paths:

// app/actions/create-post.ts
'use server';

import { ok, fail, type ActionResult } from '@/lib/action-result';
import { db } from '@/lib/db';

export async function createPost(
  _prev: ActionResult<{ id: string }> | null,
  formData: FormData
): Promise<ActionResult<{ id: string }>> {
  const start = performance.now();

  try {
    const title = formData.get('title') as string;
    if (!title?.trim()) {
      return fail('VALIDATION_ERROR', 'Title is required', performance.now() - start);
    }

    const post = await db.post.create({ data: { title } });
    return ok({ id: post.id }, performance.now() - start);
  } catch (err) {
    return fail('INTERNAL_ERROR', 'Failed to create post', performance.now() - start);
  }
}

Capturing User Context in Server Actions

Observability is only useful when you know who experienced an error. Server Actions have access to the same server context as Route Handlers, so you can read the session before running business logic.

Use Next-Auth (or any session library) to attach user context to every telemetry event:

// lib/auth-context.ts
import { auth } from '@/auth';   // next-auth v5

export type UserContext = {
  userId: string | null;
  email:  string | null;
  role:   string | null;
};

export async function getUserContext(): Promise<UserContext> {
  const session = await auth();
  return {
    userId: session?.user?.id   ?? null,
    email:  session?.user?.email ?? null,
    role:   (session?.user as { role?: string })?.role ?? null,
  };
}

Building a Telemetry Reporter

Centralise all telemetry calls in a single reporter module. This keeps observability logic out of business code and makes it easy to swap providers (Sentry, Datadog, OpenTelemetry) without touching action files.

The reporter records:

  • Errors — with user context, action name, and serialised error
  • Metrics — action name, outcome, and duration for latency percentiles
// lib/telemetry.ts
import type { UserContext } from './auth-context';

export type TelemetryEvent = {
  action:    string;
  ok:        boolean;
  durationMs: number;
  code?:     string;
  userId?:   string | null;
  email?:    string | null;
};

export async function reportEvent(event: TelemetryEvent): Promise<void> {
  // Replace with your actual provider SDK call, e.g.:
  //   Sentry.captureMessage(...) or
  //   otelMeter.histogram(...)
  if (process.env.NODE_ENV === 'production') {
    await fetch(process.env.OBSERVABILITY_INGEST_URL!, {
      method:  'POST',
      headers: { 'Content-Type': 'application/json',
                 'X-Api-Key':   process.env.OBSERVABILITY_API_KEY! },
      body:    JSON.stringify({ ...event, timestamp: Date.now() }),
    });
  } else {
    console.info('[telemetry]', event);
  }
}

export async function reportError(
  action: string,
  err: unknown,
  ctx: UserContext,
  durationMs: number
): Promise<void> {
  const message = err instanceof Error ? err.message : String(err);
  console.error(`[action:${action}] ${message}`, { userId: ctx.userId });

  await reportEvent({
    action, ok: false, durationMs,
    code:   'INTERNAL_ERROR',
    userId: ctx.userId,
    email:  ctx.email,
  });
}

Composing the Full Instrumented Action

Now combine user context, duration measurement, and telemetry reporting into one coherent Server Action. The key rule is: always report before returning — even on the happy path, so you have latency data for successful calls too.

// app/actions/create-post.ts  (final version)
'use server';

import { ok, fail, type ActionResult } from '@/lib/action-result';
import { getUserContext }              from '@/lib/auth-context';
import { reportEvent, reportError }   from '@/lib/telemetry';
import { db }                         from '@/lib/db';

export async function createPost(
  _prev: ActionResult<{ id: string }> | null,
  formData: FormData
): Promise<ActionResult<{ id: string }>> {
  const start = performance.now();
  const ctx   = await getUserContext();

  try {
    const title = (formData.get('title') as string)?.trim();
    if (!title) {
      const dur = performance.now() - start;
      await reportEvent({ action: 'createPost', ok: false,
                          code: 'VALIDATION_ERROR', durationMs: dur,
                          userId: ctx.userId });
      return fail('VALIDATION_ERROR', 'Title is required', dur);
    }

    const post = await db.post.create({ data: { title, authorId: ctx.userId! } });
    const dur  = performance.now() - start;

    await reportEvent({ action: 'createPost', ok: true,
                        durationMs: dur, userId: ctx.userId });
    return ok({ id: post.id }, dur);
  } catch (err) {
    const dur = performance.now() - start;
    await reportError('createPost', err, ctx, dur);
    return fail('INTERNAL_ERROR', 'Failed to create post', dur);
  }
}

Extracting a withAction Higher-Order Wrapper

Duplicating context-fetch and telemetry calls in every action creates noise and inconsistency. Extract a higher-order wrapper, withAction, that handles the boilerplate automatically.

The wrapper:

  • Reads user context once at the start
  • Measures duration around the inner function
  • Reports success and failure telemetry
  • Translates unknown thrown errors into safe ActionFailure values
// lib/with-action.ts
import { ok, fail, type ActionResult } from './action-result';
import { getUserContext, type UserContext } from './auth-context';
import { reportEvent, reportError }        from './telemetry';

type InnerFn<TInput, TOutput> = (
  input: TInput,
  ctx:   UserContext
) => Promise<ActionResult<TOutput>>;

export function withAction<TInput, TOutput>(
  name:  string,
  inner: InnerFn<TInput, TOutput>
) {
  return async (input: TInput): Promise<ActionResult<TOutput>> => {
    const start = performance.now();
    const ctx   = await getUserContext();

    try {
      const result = await inner(input, ctx);
      const dur    = performance.now() - start;

      await reportEvent({
        action: name, ok: result.ok,
        code:   result.ok ? undefined : (result as { code: string }).code,
        durationMs: dur, userId: ctx.userId,
      });
      return result;
    } catch (err) {
      const dur = performance.now() - start;
      await reportError(name, err, ctx, dur);
      return fail('INTERNAL_ERROR', 'An unexpected error occurred', dur);
    }
  };
}

Using withAction in a Real Server Action

With withAction in place, individual action files become concise and focused purely on business logic. All observability wiring is invisible to the developer writing the action.

// app/actions/delete-post.ts
'use server';

import { ok, fail } from '@/lib/action-result';
import { withAction }  from '@/lib/with-action';
import { db }          from '@/lib/db';

export const deletePost = withAction(
  'deletePost',
  async ({ postId }: { postId: string }, ctx) => {
    if (!ctx.userId) {
      return fail('UNAUTHENTICATED', 'You must be logged in', 0);
    }

    const post = await db.post.findUnique({ where: { id: postId } });
    if (!post) {
      return fail('NOT_FOUND', 'Post not found', 0);
    }
    if (post.authorId !== ctx.userId) {
      return fail('FORBIDDEN', 'You do not own this post', 0);
    }

    await db.post.delete({ where: { id: postId } });
    return ok({ deleted: true }, 0);   // duration injected by wrapper
  }
);

// Usage in a Client Component:
// const result = await deletePost({ postId: '...' });
// if (!result.ok) toast.error(result.message);

Connecting to OpenTelemetry Traces

Metric events are useful, but distributed traces let you see exactly where time was spent across DB queries, external API calls, and middleware. Next.js 15 has built-in OpenTelemetry support via the instrumentation.ts file.

Register your OTel SDK once, then create spans inside actions to complement the automatic HTTP spans:

// instrumentation.ts  (Next.js 15 — runs once on server start)
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { NodeSDK }             = await import('@opentelemetry/sdk-node');
    const { OTLPTraceExporter }   = await import('@opentelemetry/exporter-trace-otlp-http');
    const { Resource }            = await import('@opentelemetry/resources');
    const { SEMRESATTRS_SERVICE_NAME } = await import('@opentelemetry/semantic-conventions');

    const sdk = new NodeSDK({
      resource: new Resource({
        [SEMRESATTRS_SERVICE_NAME]: 'my-nextjs-app',
      }),
      traceExporter: new OTLPTraceExporter({
        url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
      }),
    });

    sdk.start();
  }
}

Adding Custom Spans Inside withAction

Once OTel is registered, you can enrich traces with custom spans directly inside the withAction wrapper. This creates a parent span for the entire action, which automatically parents any child spans created by DB clients or fetch calls during the action's execution.

// lib/with-action.ts  (OTel-enhanced version)
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { ok, fail, type ActionResult } from './action-result';
import { getUserContext, type UserContext } from './auth-context';
import { reportEvent, reportError }        from './telemetry';

const tracer = trace.getTracer('server-actions');

type InnerFn<I, O> = (input: I, ctx: UserContext) => Promise<ActionResult<O>>;

export function withAction<I, O>(name: string, inner: InnerFn<I, O>) {
  return async (input: I): Promise<ActionResult<O>> => {
    return tracer.startActiveSpan(`action.${name}`, async (span) => {
      const start = performance.now();
      const ctx   = await getUserContext();

      span.setAttributes({
        'action.name':   name,
        'user.id':       ctx.userId ?? 'anonymous',
        'user.email':    ctx.email  ?? '',
      });

      try {
        const result = await inner(input, ctx);
        const dur    = performance.now() - start;

        span.setAttributes({ 'action.ok': result.ok, 'action.duration_ms': dur });
        span.setStatus({ code: result.ok ? SpanStatusCode.OK : SpanStatusCode.ERROR });
        span.end();

        await reportEvent({ action: name, ok: result.ok, durationMs: dur, userId: ctx.userId });
        return result;
      } catch (err) {
        const dur = performance.now() - start;
        span.recordException(err as Error);
        span.setStatus({ code: SpanStatusCode.ERROR });
        span.end();

        await reportError(name, err, ctx, dur);
        return fail('INTERNAL_ERROR', 'An unexpected error occurred', dur);
      }
    });
  };
}

Surfacing Action Errors in a Client Error Boundary

Even with perfect server-side telemetry, the client still needs to handle failures gracefully. Use useActionState to relay the ActionResult back to the UI, and pair it with a React Error Boundary for unexpected render errors:

  • useActionState propagates the ActionFailure payload so you can show inline messages
  • A top-level error.tsx in the App Router catches errors that escape action boundaries (e.g. during streaming)
  • Log client-side boundary triggers back to the same telemetry pipeline via a dedicated API route to keep a unified error view

This completes the loop: errors are captured on the server with full context and surfaced on the client with safe, user-friendly messages.

Knowledge Check: Telemetry in Server Actions

Test your understanding of the key design decisions in this lesson.

Lesson Recap: Server Action Telemetry Pipeline

In this lesson you built a complete observability pipeline for Next.js 15 Server Actions:

  • ActionResult discriminated union — a consistent, serialisable return type that distinguishes success from failure without exposing server internals to the client
  • Duration measurement — performance.now() wrapping every action body, captured on both success and error paths
  • User context — reading the session once at the start of each action so every telemetry event carries userId and email
  • Centralised telemetry reporter — a single module that forwards structured events to your observability backend, keeping business logic clean
  • withAction wrapper — a higher-order function that composes context, duration, and telemetry automatically so individual actions stay focused on business logic
  • OpenTelemetry integration — registering the OTel SDK in instrumentation.ts and creating parent spans inside withAction so DB and external API calls appear as child spans in distributed traces
  • Client error surfacing — using useActionState and error.tsx to relay safe failure messages to users while keeping raw errors server-side

Together these layers give you full visibility into every action invocation — who triggered it, whether it succeeded, how long it took, and exactly where it failed.

자주 묻는 질문

“서버 액션 실패와 원격 측정 데이터 수집” 강의는 무료인가요?

네 — “서버 액션 실패와 원격 측정 데이터 수집” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Next.js 15 Fullstack (App Router + Server Actions) 강의 전체를 잠금 해제할 수 있습니다. Next.js 15 Fullstack (App Router + Server Actions) 강의에는 총 4개의 강의가 포함되어 있습니다.

“서버 액션 실패와 원격 측정 데이터 수집”에서 뭘 배우나요?

사용자 컨텍스트와 함께 액션 오류 및 성능 지표를 관측성 백엔드에 보고하는 방법을 배웁니다. 브라우저에서 직접 실행하는 실습 코드로 Next.js 15 Fullstack (App Router + Server Actions)을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Next.js 15 Fullstack (App Router + Server Actions)을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Next.js 15 Fullstack (App Router + Server Actions)은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“서버 액션 실패와 원격 측정 데이터 수집” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Next.js 15 Fullstack (App Router + Server Actions) 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Next.js 15 Fullstack (App Router + Server Actions) 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. instrumentation.ts를 활용한 OpenTelemetry 추적
  2. 세밀한 error.tsx와 전역 오류 경계
  3. 서버와 Edge 전반의 구조화된 로깅
  4. 서버 액션 실패와 원격 측정 데이터 수집
← Next.js 15 Fullstack (App Router + Server Actions)(으)로 돌아가기