inngest / inngest-js

The developer platform for easily building reliable workflows with zero infrastructure for TypeScript & JavaScript
https://www.inngest.com/
GNU General Public License v3.0
436 stars 42 forks source link

[BUG] Sentry Middleware doesn't split steps into their own spans #662

Open TaylorFacen opened 3 months ago

TaylorFacen commented 3 months ago

Describe the bug I copied the code used in the @innest/sentry-middleware package to my codebase to make some modifications. When a function runs, I only see a span for the entire Inngest Function Run span and not spans for each step within a function.

To Reproduce Steps to reproduce the behavior:

Use the following code: middleware/sentry-middleware.ts

import { InngestMiddleware } from "inngest";
import * as Sentry from "@sentry/nextjs";

export const sentryMiddleware = new InngestMiddleware({
  name: 'Sentry Middleware',
  init({ client }) {
    return {
      onFunctionRun({ ctx, fn, steps }) {
        const functionId = fn.id(client.id);
        const runId = ctx.runId;
        return Sentry.withScope(scope => {
          const sharedTags: Record<string, string> = {
            "inngest.client.id": client.id,
            "inngest.function.id": functionId,
            "inngest.function.name": fn.name,
            "inngest.event.name": ctx.event.name,
            "inngest.run.id": runId,
            "inngest.run.url": `https://app.inngest.com/env/production/functions/${functionId}/logs/${runId}`
          };

          scope.setTags(sharedTags);

          if (ctx.event.user) { scope.setUser({ id: ctx.event.user.id }) }

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

          return Sentry.startSpanManual(
            {
              name: "Inngest Function Run",
              op: "run",
              attributes: {
                ...sharedTags,
                "inngest.event": JSON.stringify(ctx.event),
              },
              scope,
            },
            reqSpan => {
              return {
                transformInput() {
                  return {
                    ctx: {
                      runId,
                      functionId,
                      sentry: Sentry,
                    },
                  };
                },
                beforeMemoization() {
                  Sentry.withActiveSpan(reqSpan, (scope) => {
                    Sentry.startSpanManual(
                      {
                        name: "Memoization",
                        op: "memoization",
                        attributes: {
                          ...sharedTags,
                          "inngest.memoization.count": steps.length,
                        },
                        scope,
                      },
                      (_memoSpan) => {
                        memoSpan = _memoSpan;
                      }
                    );
                  });
                },
                afterMemoization() {
                  memoSpan?.end();
                },
                beforeExecution() {
                  Sentry.withActiveSpan(reqSpan, (scope) => {
                    Sentry.startSpanManual(
                      {
                        name: "Execution",
                        op: "execution",
                        attributes: {
                          ...sharedTags,
                        },
                        scope,
                      },
                      (_execSpan) => {
                        execSpan = _execSpan;
                      }
                    );
                  });
                },
                afterExecution() {
                  execSpan?.end();
                },
                transformOutput({ result, step }) {
                  // Set step metadata
                  if (step) {
                    console.log({ step })
                    Sentry.withActiveSpan(reqSpan, (scope) => {
                      sharedTags["inngest.step.name"] =
                        step.displayName ?? "";
                      if ('id' in step) { sharedTags["inngest.step.id"] = step.id as string }
                      sharedTags["inngest.step.op"] = step.op;

                      scope.setTags(sharedTags);
                    });
                  }

                  // Capture step output and log errors
                  if (result.error) {
                    reqSpan.setStatus({
                      code: 2,
                    });

                    Sentry.withActiveSpan(reqSpan, (scope) => {
                      scope.setTags(sharedTags);
                      scope.captureException(result.error);
                    });
                  } else {
                    reqSpan.setStatus({
                      code: 1,
                    });
                  }
                },
                async beforeResponse() {
                  reqSpan.end();
                  await Sentry.flush();
                },
              }
            }
          )
        })
      }
    }
  }
});

and run a function with multiple steps. user-updated.ts

import { inngest } from "@/lib/inngest/clients";
import { updateCustomer } from '@/lib/stripe-node';
import { upsertPlainCustomerFromUser } from "@/lib/plain";
import { slugify } from "inngest";
import { db } from "@/db";

const name = "User Updated";
export const updated = inngest.createFunction(
  { name, id: slugify(name) },
  { event: 'app/user.updated' },
  async ({ event, step }) => {
    const { data: { data }, user: { id: userId } } = event;
    const updatedFields = data.map(({ field }) => field)

    const user = await step.run({ id: 'fetch-user' }, async () =>
      db.user.findFirstOrThrow({ where: { id: userId } })
    )

    if (updatedFields.includes('name')) {
      await step.run({ id: 'update-stripe-customer' }, async () => {
        // throw new Error('Example Inngest error')
        return updateStripeCustomer({
          customerId: user.stripeCustomerId,
          properties: { name: user.name || "" }
        })
      }

      );

      await step.run({ id: 'update-plain-customer' }, async () =>
        upsertPlainCustomerFromUser({ user, customerId: user.plainCustomerId || undefined })
      )
    }
  }
)

Expected behavior Each step has an execution span

Code snippets / Logs / Screenshots

CleanShot 2024-07-23 at 16 32 14@2x

System info (please complete the following information):

linear[bot] commented 3 months ago

INN-3350 [BUG] Sentry Middleware doesn't split steps into their own spans