HOW IT WORKS

Magpie in 10 minutes.

No SDK. No config file. One function call.

If you have ever fought a YAML file at 11pm, you are in the right place.

Step 1

Create your project

Sign up and create a project in the Magpie dashboard. You will get a Project ID and an API key.

Keep your API key in environment variables. Never commit it.

Step 2

Add your environment variables

Create a .env (or .env.local) file in your app and paste this:

MAGPIE_BASE_URL=https://usemagpie.dev
MAGPIE_PROJECT_ID=proj_...
MAGPIE_API_KEY=magpie_...

Use your Magpie deployment URL for MAGPIE_BASE_URL (no trailing slash).

Step 3

Drop in the helper

Copy this function into your backend. Works with Express, Next.js API routes, Fastify, or anything Node.js.

import { initObserver } from './Magpie'

const { track, trackError } = initObserver(
  {
    projectId: process.env.MAGPIE_PROJECT_ID!,
    apiKey: process.env.MAGPIE_API_KEY!,
    run: process.env.NODE_ENV === 'production',
  })

Step 4

Track requests with one middleware

Use track() once as a global middleware (like Morgan), so you do not need to add it in every route handler. Pass a stable action name, then optionally enable captureRequest and captureResponse to capture payloads with automatic recursive redaction of sensitive fields.

You can also instrument selected routes directly when you want specific action names (for example checkout.completed). Most teams use a hybrid approach: one global middleware for baseline coverage plus route-specific actions on critical flows.

// Express global middleware (Morgan-style)
app.use(req, res, next) => {
  req.magpieStartedAt = Date.now()
  track(req, res, 'http.request', {
    captureRequest: true,
    captureResponse: true
  })
  next()
})

// Routes stay clean
app.post('/assign-driver', async (req, res) => {
  try {
    // your logic
  } catch (err) {
    trackError(err, req)
    res.status(500).json({ error: 'failed' })
  }
})
Step 5

Track errors

Call trackError() in your catch blocks. Errors are linked to the request that caused them, grouped by fingerprint, and appear inline on the user timeline and the dedicated Errors view.

If you prefer cleaner route handlers, you can call trackError() from one centralized error middleware instead.

Error tracking costs zero tokens.

Step 6

Open the timeline

In the Magpie dashboard, search any userId to see their complete request history: every action, every failure, in order.

Baselines are computed from successful requests over the last 7 days, and requests that are 2x slower than p95 are automatically flagged as slow so your team can spot regressions early.

This is the view your whole team can read. No translation layer. No "ask an engineer what this span means."

The complete helper file

Copy this into your project as Magpie.ts (or keep the filename you like. We ship the same thing as track.example.ts in this repo).

Below is the real implementation: same types, same ingest URLs, same behaviour you get in production.

import { randomUUID } from "node:crypto";
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";

export type MagpieObserverConfig = {
  /** Project id from the Magpie dashboard (included in event `meta` for your own debugging). */
  projectId: string;
  /** Project API key (sent as `x-api-key` to Magpie). */
  apiKey: string;
  /** Set to `false` to disable observer emissions (useful outside production). */
  run?: boolean;
};

type RequestLike = Pick<IncomingMessage, "method" | "headers" | "url"> & {
  originalUrl?: string;
  baseUrl?: string;
  body?: unknown;
  magpieStartedAt?: number;
  magpieRequestId?: string;
  user?: { id?: string; sub?: string };
};

type ResponseLike = (Pick<ServerResponse, "statusCode"> & {
  json?: (body: unknown) => unknown;
  send?: (body: unknown) => unknown;
  on?: (event: string, listener: () => void) => unknown;
  once?: (event: string, listener: () => void) => unknown;
  off?: (event: string, listener: () => void) => unknown;
  removeListener?: (event: string, listener: () => void) => unknown;
  magpieResponseBody?: unknown;
  magpieResponseCapturePatched?: boolean;
  magpieTrackEmissionRegistered?: boolean;
  magpieTrackEmitted?: boolean;
}) | { status?: number; json?: (body: unknown) => unknown; send?: (body: unknown) => unknown; on?: (event: string, listener: () => void) => unknown; once?: (event: string, listener: () => void) => unknown; off?: (event: string, listener: () => void) => unknown; removeListener?: (event: string, listener: () => void) => unknown; magpieResponseBody?: unknown; magpieResponseCapturePatched?: boolean; magpieTrackEmissionRegistered?: boolean; magpieTrackEmitted?: boolean };

type TrackOptions = {
  userId?: string;
  captureRequest?: boolean;
  captureResponse?: boolean;
};

const REDACTED_VALUE = "[redacted]";
const CIRCULAR_VALUE = "[circular]";
const MAX_DEPTH_VALUE = "[max-depth]";
const TRUNCATED_VALUE = "[truncated]";
const SCRUB_MAX_DEPTH = 8;
const SCRUB_MAX_ARRAY_LENGTH = 100;
const SCRUB_MAX_OBJECT_KEYS = 100;
const SENSITIVE_KEY_NAMES = new Set([
  "password",
  "token",
  "secret",
  "authorization",
  "auth",
  "apikey",
  "api_key",
  "card",
  "cvv",
  "pin",
  "otp",
  "ssn",
  "bvn",
  "nin",
]);

function scrubSensitivePayload(
  value: unknown,
  seen: WeakSet<object> = new WeakSet(),
  depth = 0,
): unknown {
  if (
    value != null &&
    typeof value === "object" &&
    typeof (value as { toObject?: unknown }).toObject === "function"
  ) {
    value = (value as { toObject: () => unknown }).toObject();
  }

  if (depth >= SCRUB_MAX_DEPTH) {
    return {
      __magpie: MAX_DEPTH_VALUE,
      depth,
      type: Array.isArray(value) ? "array" : typeof value,
    };
  }

  if (Array.isArray(value)) {
    if (seen.has(value)) {
      return CIRCULAR_VALUE;
    }
    seen.add(value);

    const trimmed = value.slice(0, SCRUB_MAX_ARRAY_LENGTH);
    const output = trimmed.map((item) => scrubSensitivePayload(item, seen, depth + 1));
    if (value.length > SCRUB_MAX_ARRAY_LENGTH) {
      output.push({
        __magpie: TRUNCATED_VALUE,
        kind: "array",
        kept: SCRUB_MAX_ARRAY_LENGTH,
        total: value.length,
      });
    }
    return output;
  }

  if (value && typeof value === "object") {
    const objectValue = value as object;
    if (seen.has(objectValue)) {
      return CIRCULAR_VALUE;
    }
    seen.add(objectValue);

    const input = value as Record<string, unknown>;
    const output: Record<string, unknown> = {};
    const entries = Object.entries(input);
    for (const [key, raw] of entries.slice(0, SCRUB_MAX_OBJECT_KEYS)) {
      if (SENSITIVE_KEY_NAMES.has(key.toLowerCase())) {
        output[key] = REDACTED_VALUE;
      } else {
        output[key] = scrubSensitivePayload(raw, seen, depth + 1);
      }
    }
    if (entries.length > SCRUB_MAX_OBJECT_KEYS) {
      output.__magpie = {
        value: TRUNCATED_VALUE,
        kind: "object",
        kept: SCRUB_MAX_OBJECT_KEYS,
        total: entries.length,
      };
    }
    return output;
  }
  return value;
}

function normaliseBaseUrl(baseUrl: string) {
  return baseUrl.replace(/\/+$/, "");
}

function headerValue(
  headers: IncomingHttpHeaders | Headers,
  name: string,
): string | undefined {
  if (headers instanceof Headers) {
    const v = headers.get(name);
    return v?.trim() || undefined;
  }
  const lower = name.toLowerCase();
  const raw = headers[lower] ?? headers[name];
  if (Array.isArray(raw)) {
    return raw[0]?.trim() || undefined;
  }
  return typeof raw === "string" ? raw.trim() || undefined : undefined;
}

function inferRoute(req: RequestLike): string | undefined {
  const raw = req.originalUrl ?? req.url ?? "";
  try {
    const pathname = raw.startsWith("http") ? new URL(raw).pathname : raw.split("?")[0];
    const path = pathname || "/";
    const base = req.baseUrl ?? "";
    if (base && path.startsWith(base)) {
      return path || undefined;
    }
    return `${base}${path}` || undefined;
  } catch {
    return raw.split("?")[0] || undefined;
  }
}

function inferStatus(res: ResponseLike): number | undefined {
  if ("statusCode" in res && typeof res.statusCode === "number") {
    return res.statusCode;
  }
  if ("status" in res && typeof (res as { status?: unknown }).status === "number") {
    return (res as { status: number }).status;
  }
  return undefined;
}

function inferUserId(req: RequestLike): string | undefined {
  const h = req.headers;
  return (
    headerValue(h, "x-user-id") ??
    headerValue(h, "x-magpie-user-id") ??
    req.user?.id ??
    req.user?.sub
  );
}

function inferRequestId(req: RequestLike): string {
  if (req.magpieRequestId) {
    return req.magpieRequestId;
  }
  const h = req.headers;
  const requestId = headerValue(h, "x-request-id") ?? randomUUID();
  req.magpieRequestId = requestId;
  return requestId;
}

function inferDurationMs(req: RequestLike): number | undefined {
  const start = req.magpieStartedAt;
  if (typeof start !== "number" || !Number.isFinite(start)) {
    return undefined;
  }
  const ms = Date.now() - start;
  return Number.isFinite(ms) ? ms : undefined;
}

function fireAndForget(promise: Promise<unknown>) {
  void promise.catch(() => {
    /* ignore */
  });
}

function inferSafeRequestHeaders(headers: IncomingHttpHeaders | Headers): Record<string, string> {
  const blocked = new Set(["authorization", "cookie", "x-api-key"]);
  const entries: Array<[string, string]> = [];
  if (headers instanceof Headers) {
    headers.forEach((value, key) => {
      const normalisedKey = key.toLowerCase();
      if (!blocked.has(normalisedKey)) {
        entries.push([key, value]);
      }
    });
  } else {
    for (const [key, raw] of Object.entries(headers)) {
      const normalisedKey = key.toLowerCase();
      if (blocked.has(normalisedKey) || raw == null) {
        continue;
      }
      if (Array.isArray(raw)) {
        entries.push([key, raw.join(", ")]);
        continue;
      }
      if (typeof raw === "string") {
        entries.push([key, raw]);
      }
    }
  }
  return Object.fromEntries(entries);
}

function patchResponseCapture(res: ResponseLike) {
  if (res.magpieResponseCapturePatched) {
    return;
  }
  res.magpieResponseCapturePatched = true;

  const originalJson = typeof res.json === "function" ? res.json.bind(res) : undefined;
  const originalSend = typeof res.send === "function" ? res.send.bind(res) : undefined;

  if (originalJson) {
    res.json = (body: unknown) => {
      res.magpieResponseBody = scrubSensitivePayload(body);
      return originalJson(body);
    };
  }

  if (originalSend) {
    res.send = (body: unknown) => {
      res.magpieResponseBody = scrubSensitivePayload(body);
      return originalSend(body);
    };
  }
}

function sendTrackedEvent(eventUrl: string, apiKey: string, body: Record<string, unknown>) {
  fireAndForget(
    fetch(eventUrl, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "x-api-key": apiKey,
      },
      body: JSON.stringify(body),
    }),
  );
}

function canRegisterLifecycle(res: ResponseLike): boolean {
  return typeof res.once === "function" || typeof res.on === "function";
}

function detachLifecycleListener(
  res: ResponseLike,
  event: string,
  listener: () => void,
) {
  if (typeof res.off === "function") {
    res.off(event, listener);
    return;
  }
  if (typeof res.removeListener === "function") {
    res.removeListener(event, listener);
  }
}

export function initObserver(config: MagpieObserverConfig) {
  if (config.run === false) {
    return {
      track: () => {
        /* observer disabled */
      },
      trackError: () => {
        /* observer disabled */
      },
    };
  }

  if (!config.projectId?.trim()) {
    throw new Error("Magpie: `projectId` is required.");
  }
  if (!config.apiKey?.trim()) {
    throw new Error("Magpie: `apiKey` is required.");
  }
  const base = normaliseBaseUrl(process.env.MAGPIE_BASE_URL ?? "");
  if (!base) {
    throw new Error(
      "Magpie: set MAGPIE_BASE_URL to your Magpie deployment origin (e.g. https://app.example.com)",
    );
  }
  const eventUrl = `${base}/api/ingest/event`;
  const errorUrl = `${base}/api/ingest/error`;

  function track(
    req: RequestLike,
    res: ResponseLike,
    action?: string,
    options?: TrackOptions,
  ) {
    if (res.magpieTrackEmissionRegistered) {
      return;
    }
    res.magpieTrackEmissionRegistered = true;

    if (options?.captureResponse) {
      patchResponseCapture(res);
    }

    const emitTrackedEvent = () => {
      if (res.magpieTrackEmitted) {
        return;
      }
      res.magpieTrackEmitted = true;

      const body = {
        requestId: inferRequestId(req),
        route: inferRoute(req),
        method: req.method,
        status: inferStatus(res),
        duration: inferDurationMs(req),
        userId: options?.userId ?? inferUserId(req),
        action,
        requestHeaders: inferSafeRequestHeaders(req.headers),
        requestBody: options?.captureRequest ? scrubSensitivePayload(req.body ?? null) : null,
        responseBody: options?.captureResponse ? (res.magpieResponseBody ?? null) : null,
        timestamp: new Date().toISOString(),
        meta: { projectId: config.projectId },
      };

      sendTrackedEvent(eventUrl, config.apiKey, body);
    };

    if (!options?.captureResponse) {
      emitTrackedEvent();
      return;
    }

    if (!canRegisterLifecycle(res)) {
      emitTrackedEvent();
      return;
    }

    const onDone = () => {
      detachLifecycleListener(res, "finish", onDone);
      detachLifecycleListener(res, "close", onDone);
      emitTrackedEvent();
    };

    if (typeof res.once === "function") {
      res.once("finish", onDone);
      res.once("close", onDone);
      return;
    }

    if (typeof res.on === "function") {
      res.on("finish", onDone);
      res.on("close", onDone);
      return;
    }

    emitTrackedEvent();
  }

  function trackError(error: unknown, req: RequestLike) {
    const err = error instanceof Error ? error : new Error(String(error));
    const body = {
      requestId: inferRequestId(req),
      errorMessage: err.message,
      stackSummary: err.stack,
      timestamp: new Date().toISOString(),
    };

    fireAndForget(
      fetch(errorUrl, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          "x-api-key": config.apiKey,
        },
        body: JSON.stringify(body),
      }),
    );
  }

  return { track, trackError };
}

Supported environments

If it speaks HTTP and runs on Node, you are probably fine.

Express.jsNext.js API RoutesFastifyNestJSAny Node.js HTTP server

More runtimes coming. Bun and Deno support in progress.