Example middleware
v2.0.0+

The following examples show how you might use middleware in some real-world scenarios.

Sentry error reporting and tracing

This example uses Sentry to:

  • Capture exceptions for reporting
  • Add tracing to each function run
  • Include useful context for each exception and trace like function ID and event names
import * as Sentry from "@sentry/node";

const sentryMiddleware = new InngestMiddleware({
  name: "Sentry Middleware",
  init({ client }) {
    // Initialize Sentry as soon as possible, creating a hub
    Sentry.init({ dsn: "..." });

    // Set up some tags that will be applied to all events
    Sentry.setTag("inngest.client.name", client.name);

    return {
      onFunctionRun({ ctx, fn }) {
        // Add specific context for the given function run
        Sentry.setTags({
          "inngest.function.id": fn.id(client.name),
          "inngest.function.name": fn.name,
          "inngest.event": ctx.event.name,
          "inngest.run.id": ctx.runId,
        });

        // Start a transaction for this run
        const transaction = Sentry.startTransaction({
          name: "Inngest Function Run",
          op: "run",
          data: ctx.event,
        });

        let memoSpan: Sentry.Span;
        let execSpan: Sentry.Span;

        return {
          transformInput() {
            return {
              ctx: {
                // Add the Sentry client to the input arg so our
                // functions can use it directly too
                sentry: Sentry.getCurrentHub(),
              },
            };
          },
          beforeMemoization() {
            // Track different spans for memoization and execution
            memoSpan = transaction.startChild({ op: "memoization" });
          },
          afterMemoization() {
            memoSpan.finish();
          },
          beforeExecution() {
            execSpan = transaction.startChild({ op: "execution" });
          },
          afterExecution() {
            execSpan.finish();
          },
          transformOutput({ result, step }) {
            // Capture step output and log errors
            if (step) {
              Sentry.setTags({
                "inngest.step.name": step.name,
                "inngest.step.op": step.op,
              });

              if (result.error) {
                Sentry.captureException(result.error);
              }
            }
          },
          async beforeResponse() {
            // Finish the transaction and flush data to Sentry before the
            // request closes
            transaction.finish();
            await Sentry.flush();
          },
        };
      },
    };
  },
});

Prisma in function context

The following is an example of adding a Prisma client to all Inngest functions, allowing them immediate access without needing to create the client themselves.

While this example uses Prisma, it serves as a good example of using the onFunctionRun -> input hook to mutate function input to perform crucial setup for your functions and keep them to just business logic.

💡 Types are inferred from middleware outputs, so your Inngest functions will see an appropriately-typed prisma property in their input.

import { PrismaClient } from "@prisma/client";

const prismaMiddleware = new InngestMiddleware({
  name: "Prisma Middleware",
  init() {
    const prisma = new PrismaClient();

    return {
      onFunctionRun(ctx) {
        return {
          transformInput(ctx) {
            return {
              // Anything passed via `ctx` will be merged with the function's arguments
              ctx: {
                prisma,
              },
            };
          },
        };
      },
    };
  },
});

// When defining a function...
inngest.createFunction(
  { name: "Example" },
  { event: "app/user.loggedin" },
  async ({ prisma }) => {
    await prisma.auditTrail.create(/* ... */);
  }
);

Logging

The following shows you how you can create a logger middleware and customize it to your needs.

It is based on the built-in logger middleware in the SDK, and hope it gives you an idea of what you can do if the built-in logger doesn't meet your needs.

new InngestMiddleware({
  name: "Inngest: Logger",
  init({ client }) {
    return {
      onFunctionRun(arg) {
        const { ctx } = arg;
        const metadata = {
          runID: ctx.runId,
          eventName: ctx.event.name,
          functionName: arg.fn.name,
        };

        let providedLogger: Logger = client["logger"];
        // create a child logger if the provided logger has child logger implementation
        try {
          if ("child" in providedLogger) {
            type ChildLoggerFn = (
              metadata: Record<string, unknown>
            ) => Logger;
            providedLogger = (providedLogger.child as ChildLoggerFn)(metadata)
          }
        } catch (err) {
          console.error('failed to create "childLogger" with error: ', err);
          // no-op
        }
        const logger = new ProxyLogger(providedLogger);

        return {
          transformInput() {
            return {
              ctx: {
                /**
                 * The passed in logger from the user.
                 * Defaults to a console logger if not provided.
                 */
                logger,
              },
            };
          },
          beforeExecution() {
            logger.enable();
          },
          transformOutput({ result: { error } }) {
            if (error) {
              logger.error(error);
            }
          },
          async beforeResponse() {
            await logger.flush();
          },
        };
      },
    };
  },
})