From 5f4d35e0838d3adec705856da830e4104fbd38da Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Thu, 31 Jul 2025 16:22:55 -0700 Subject: [PATCH] Add tracing to ESBuild --- .../backend/src/app/api/latest/users/crud.tsx | 2 +- apps/backend/src/lib/email-rendering.tsx | 4 +- apps/backend/src/lib/emails.tsx | 2 +- apps/backend/src/lib/freestyle.tsx | 4 +- apps/backend/src/lib/tokens.tsx | 2 +- apps/backend/src/prisma-client.tsx | 2 +- .../src/route-handlers/crud-handler.tsx | 2 +- .../src/route-handlers/smart-request.tsx | 2 +- .../src/route-handlers/smart-response.tsx | 2 +- .../route-handlers/smart-route-handler.tsx | 2 +- apps/backend/src/utils/telemetry.tsx | 1 + packages/stack-shared/package.json | 1 + packages/stack-shared/src/utils/esbuild.tsx | 9 +++-- packages/stack-shared/src/utils/telemetry.tsx | 37 +++++++++++++++++++ pnpm-lock.yaml | 3 ++ 15 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 packages/stack-shared/src/utils/telemetry.tsx diff --git a/apps/backend/src/app/api/latest/users/crud.tsx b/apps/backend/src/app/api/latest/users/crud.tsx index b81ef4e2c..fd91b50fe 100644 --- a/apps/backend/src/app/api/latest/users/crud.tsx +++ b/apps/backend/src/app/api/latest/users/crud.tsx @@ -7,7 +7,6 @@ import { PrismaTransaction } from "@/lib/types"; import { sendTeamMembershipDeletedWebhook, sendUserCreatedWebhook, sendUserDeletedWebhook, sendUserUpdatedWebhook } from "@/lib/webhooks"; import { RawQuery, getPrismaClientForSourceOfTruth, getPrismaClientForTenancy, getPrismaSchemaForSourceOfTruth, getPrismaSchemaForTenancy, globalPrismaClient, rawQuery, retryTransaction, sqlQuoteIdent } from "@/prisma-client"; import { createCrudHandlers } from "@/route-handlers/crud-handler"; -import { log } from "@/utils/telemetry"; import { runAsynchronouslyAndWaitUntil } from "@/utils/vercel"; import { BooleanTrue, Prisma } from "@prisma/client"; import { KnownErrors } from "@stackframe/stack-shared"; @@ -20,6 +19,7 @@ import { StackAssertionError, StatusError, captureError, throwErr } from "@stack import { hashPassword, isPasswordHashValid } from "@stackframe/stack-shared/dist/utils/hashes"; import { has } from "@stackframe/stack-shared/dist/utils/objects"; import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; +import { log } from "@stackframe/stack-shared/dist/utils/telemetry"; import { isUuid } from "@stackframe/stack-shared/dist/utils/uuids"; import { teamPrismaToCrud, teamsCrudHandlers } from "../teams/crud"; diff --git a/apps/backend/src/lib/email-rendering.tsx b/apps/backend/src/lib/email-rendering.tsx index c053e5bf0..d39ffa4d1 100644 --- a/apps/backend/src/lib/email-rendering.tsx +++ b/apps/backend/src/lib/email-rendering.tsx @@ -1,4 +1,4 @@ -import { TracedFreestyleSandboxes } from '@/lib/freestyle'; +import { Freestyle } from '@/lib/freestyle'; import { emptyEmailTheme } from '@stackframe/stack-shared/dist/helpers/emails'; import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env'; import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors'; @@ -122,7 +122,7 @@ export async function renderEmailWithTemplate( return Result.error(result.error); } - const freestyle = new TracedFreestyleSandboxes({ apiKey }); + const freestyle = new Freestyle({ apiKey }); const nodeModules = { "@react-email/components": "0.1.1", "arktype": "2.1.20", diff --git a/apps/backend/src/lib/emails.tsx b/apps/backend/src/lib/emails.tsx index ec7612cd2..e8be2f7ad 100644 --- a/apps/backend/src/lib/emails.tsx +++ b/apps/backend/src/lib/emails.tsx @@ -1,5 +1,4 @@ import { getPrismaClientForTenancy } from '@/prisma-client'; -import { traceSpan } from '@/utils/telemetry'; import { DEFAULT_TEMPLATE_IDS } from '@stackframe/stack-shared/dist/helpers/emails'; import { UsersCrud } from '@stackframe/stack-shared/dist/interface/crud/users'; import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; @@ -7,6 +6,7 @@ import { StackAssertionError, StatusError, captureError } from '@stackframe/stac import { filterUndefined, omit, pick } from '@stackframe/stack-shared/dist/utils/objects'; import { runAsynchronously, wait } from '@stackframe/stack-shared/dist/utils/promises'; import { Result } from '@stackframe/stack-shared/dist/utils/results'; +import { traceSpan } from '@stackframe/stack-shared/dist/utils/telemetry'; import nodemailer from 'nodemailer'; import { getEmailThemeForTemplate, renderEmailWithTemplate } from './email-rendering'; import { Tenancy, getTenancy } from './tenancies'; diff --git a/apps/backend/src/lib/freestyle.tsx b/apps/backend/src/lib/freestyle.tsx index bd0a4d229..5f763a9bc 100644 --- a/apps/backend/src/lib/freestyle.tsx +++ b/apps/backend/src/lib/freestyle.tsx @@ -1,8 +1,8 @@ -import { traceSpan } from '@/utils/telemetry'; import { StackAssertionError, captureError, errorToNiceString } from '@stackframe/stack-shared/dist/utils/errors'; +import { traceSpan } from '@stackframe/stack-shared/dist/utils/telemetry'; import { FreestyleSandboxes } from 'freestyle-sandboxes'; -export class TracedFreestyleSandboxes { +export class Freestyle { private freestyle: FreestyleSandboxes; constructor(options: { apiKey: string }) { diff --git a/apps/backend/src/lib/tokens.tsx b/apps/backend/src/lib/tokens.tsx index 734db4284..82d68c090 100644 --- a/apps/backend/src/lib/tokens.tsx +++ b/apps/backend/src/lib/tokens.tsx @@ -1,5 +1,4 @@ import { globalPrismaClient } from '@/prisma-client'; -import { traceSpan } from '@/utils/telemetry'; import { Prisma } from '@prisma/client'; import { KnownErrors } from '@stackframe/stack-shared'; import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; @@ -8,6 +7,7 @@ import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; import { throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { signJWT, verifyJWT } from '@stackframe/stack-shared/dist/utils/jwt'; import { Result } from '@stackframe/stack-shared/dist/utils/results'; +import { traceSpan } from '@stackframe/stack-shared/dist/utils/telemetry'; import * as jose from 'jose'; import { JOSEError, JWTExpired } from 'jose/errors'; import { SystemEventTypes, logEvent } from './events'; diff --git a/apps/backend/src/prisma-client.tsx b/apps/backend/src/prisma-client.tsx index ec84923b0..0524784cd 100644 --- a/apps/backend/src/prisma-client.tsx +++ b/apps/backend/src/prisma-client.tsx @@ -8,10 +8,10 @@ import { globalVar } from "@stackframe/stack-shared/dist/utils/globals"; import { deepPlainEquals, filterUndefined, typedFromEntries, typedKeys } from "@stackframe/stack-shared/dist/utils/objects"; import { concatStacktracesIfRejected, ignoreUnhandledRejection } from "@stackframe/stack-shared/dist/utils/promises"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; +import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; import { isPromise } from "util/types"; import { runMigrationNeeded } from "./auto-migrations"; import { Tenancy } from "./lib/tenancies"; -import { traceSpan } from "./utils/telemetry"; export type PrismaClientTransaction = PrismaClient | Parameters[0]>[0]; diff --git a/apps/backend/src/route-handlers/crud-handler.tsx b/apps/backend/src/route-handlers/crud-handler.tsx index 943ed18be..dadfda65d 100644 --- a/apps/backend/src/route-handlers/crud-handler.tsx +++ b/apps/backend/src/route-handlers/crud-handler.tsx @@ -1,7 +1,6 @@ import "../polyfills"; import { Tenancy, getSoleTenancyFromProjectBranch, } from "@/lib/tenancies"; -import { traceSpan } from "@/utils/telemetry"; import { CrudSchema, CrudTypeOf, CrudlOperation } from "@stackframe/stack-shared/dist/crud"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; @@ -10,6 +9,7 @@ import { typedIncludes } from "@stackframe/stack-shared/dist/utils/arrays"; import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { FilterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; import { deindent, typedToLowercase } from "@stackframe/stack-shared/dist/utils/strings"; +import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; import * as yup from "yup"; import { SmartRequestAuth } from "./smart-request"; import { SmartRouteHandler, createSmartRouteHandler, routeHandlerTypeHelper } from "./smart-route-handler"; diff --git a/apps/backend/src/route-handlers/smart-request.tsx b/apps/backend/src/route-handlers/smart-request.tsx index cdb9f5630..14065c807 100644 --- a/apps/backend/src/route-handlers/smart-request.tsx +++ b/apps/backend/src/route-handlers/smart-request.tsx @@ -7,7 +7,6 @@ import { getProjectQuery, listManagedProjectIds } from "@/lib/projects"; import { DEFAULT_BRANCH_ID, Tenancy, getSoleTenancyFromProjectBranch } from "@/lib/tenancies"; import { decodeAccessToken } from "@/lib/tokens"; import { globalPrismaClient, rawQueryAll } from "@/prisma-client"; -import { traceSpan, withTraceSpan } from "@/utils/telemetry"; import { KnownErrors } from "@stackframe/stack-shared"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; @@ -16,6 +15,7 @@ import { groupBy, typedIncludes } from "@stackframe/stack-shared/dist/utils/arra import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { deindent } from "@stackframe/stack-shared/dist/utils/strings"; +import { traceSpan, withTraceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; import { NextRequest } from "next/server"; import * as yup from "yup"; diff --git a/apps/backend/src/route-handlers/smart-response.tsx b/apps/backend/src/route-handlers/smart-response.tsx index 6502d3c6d..e64206a00 100644 --- a/apps/backend/src/route-handlers/smart-response.tsx +++ b/apps/backend/src/route-handlers/smart-response.tsx @@ -1,8 +1,8 @@ -import { traceSpan } from "@/utils/telemetry"; import { yupValidate } from "@stackframe/stack-shared/dist/schema-fields"; import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { Json } from "@stackframe/stack-shared/dist/utils/json"; import { deepPlainEquals } from "@stackframe/stack-shared/dist/utils/objects"; +import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; import { NextRequest } from "next/server"; import * as yup from "yup"; import "../polyfills"; diff --git a/apps/backend/src/route-handlers/smart-route-handler.tsx b/apps/backend/src/route-handlers/smart-route-handler.tsx index 55d0c9e54..8acc995c2 100644 --- a/apps/backend/src/route-handlers/smart-route-handler.tsx +++ b/apps/backend/src/route-handlers/smart-route-handler.tsx @@ -1,6 +1,5 @@ import "../polyfills"; -import { traceSpan } from "@/utils/telemetry"; import * as Sentry from "@sentry/nextjs"; import { EndpointDocumentation } from "@stackframe/stack-shared/dist/crud"; import { KnownError, KnownErrors } from "@stackframe/stack-shared/dist/known-errors"; @@ -8,6 +7,7 @@ import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/ import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; import { StackAssertionError, StatusError, captureError, errorToNiceString } from "@stackframe/stack-shared/dist/utils/errors"; import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; +import { traceSpan } from "@stackframe/stack-shared/dist/utils/telemetry"; import { NextRequest } from "next/server"; import * as yup from "yup"; import { DeepPartialSmartRequestWithSentinel, MergeSmartRequest, SmartRequest, createSmartRequest, validateSmartRequest } from "./smart-request"; diff --git a/apps/backend/src/utils/telemetry.tsx b/apps/backend/src/utils/telemetry.tsx index 50276103c..152bef3ae 100644 --- a/apps/backend/src/utils/telemetry.tsx +++ b/apps/backend/src/utils/telemetry.tsx @@ -1,6 +1,7 @@ import { Attributes, AttributeValue, Span, trace } from "@opentelemetry/api"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; + const tracer = trace.getTracer('stack-backend'); export function withTraceSpan

(optionsOrDescription: string | { description: string, attributes?: Record }, fn: (...args: P) => Promise): (...args: P) => Promise { diff --git a/packages/stack-shared/package.json b/packages/stack-shared/package.json index 16e9b3ca3..c7f27c5c5 100644 --- a/packages/stack-shared/package.json +++ b/packages/stack-shared/package.json @@ -53,6 +53,7 @@ } }, "dependencies": { + "@opentelemetry/api": "^1.9.0", "@simplewebauthn/browser": "^11.0.0", "async-mutex": "^0.5.0", "bcryptjs": "^3.0.2", diff --git a/packages/stack-shared/src/utils/esbuild.tsx b/packages/stack-shared/src/utils/esbuild.tsx index b2a1661ae..443a78780 100644 --- a/packages/stack-shared/src/utils/esbuild.tsx +++ b/packages/stack-shared/src/utils/esbuild.tsx @@ -1,8 +1,9 @@ import * as esbuild from 'esbuild-wasm/lib/browser.js'; import { join } from 'path'; +import { isBrowserLike } from './env'; import { StackAssertionError, throwErr } from "./errors"; import { Result } from "./results"; -import { isBrowserLike } from './env'; +import { traceSpan, withTraceSpan } from './telemetry'; let esbuildInitializePromise: Promise | null = null; // esbuild requires self property to be set, and it is not set by default in nodejs @@ -10,7 +11,7 @@ let esbuildInitializePromise: Promise | null = null; export async function initializeEsbuild() { if (!esbuildInitializePromise) { - esbuildInitializePromise = (async () => { + esbuildInitializePromise = withTraceSpan('initializeEsbuild', async () => { await esbuild.initialize(isBrowserLike() ? { wasmURL: `https://unpkg.com/esbuild-wasm@${esbuild.version}/esbuild.wasm`, } : { @@ -48,7 +49,7 @@ export async function bundleJavaScript(sourceFiles: Record & { ' ]); let result; try { - result = await esbuild.build({ + result = await traceSpan('bundleJavaScript', async () => await esbuild.build({ entryPoints: ['/entry.js'], bundle: true, write: false, @@ -105,7 +106,7 @@ export async function bundleJavaScript(sourceFiles: Record & { ' }, }, ], - }); + })); } catch (e) { if (e instanceof Error && e.message.startsWith("Build failed with ")) { return Result.error(e.message); diff --git a/packages/stack-shared/src/utils/telemetry.tsx b/packages/stack-shared/src/utils/telemetry.tsx new file mode 100644 index 000000000..53112df19 --- /dev/null +++ b/packages/stack-shared/src/utils/telemetry.tsx @@ -0,0 +1,37 @@ +import { Attributes, AttributeValue, Span, trace } from "@opentelemetry/api"; +import { getEnvVariable } from "./env"; +import { StackAssertionError } from "./errors"; + +const tracer = trace.getTracer('stack-tracer'); + +export function withTraceSpan

(optionsOrDescription: string | { description: string, attributes?: Record }, fn: (...args: P) => Promise): (...args: P) => Promise { + return async (...args: P) => { + return await traceSpan(optionsOrDescription, (span) => fn(...args)); + }; +} + +export async function traceSpan(optionsOrDescription: string | { description: string, attributes?: Record }, fn: (span: Span) => Promise): Promise { + let options = typeof optionsOrDescription === 'string' ? { description: optionsOrDescription } : optionsOrDescription; + return await tracer.startActiveSpan(`STACK: ${options.description}`, async (span) => { + if (options.attributes) { + for (const [key, value] of Object.entries(options.attributes)) { + span.setAttribute(key, value); + } + } + try { + return await fn(span); + } finally { + span.end(); + } + }); +} + +export function log(message: string, attributes: Attributes) { + const span = trace.getActiveSpan(); + if (span) { + span.addEvent(message, attributes); + // Telemetry is not initialized while seeding, so we don't want to throw an error + } else if (getEnvVariable('STACK_SEED_MODE', 'false') !== 'true') { + throw new StackAssertionError('No active span found'); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f25d7032d..99807b663 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1416,6 +1416,9 @@ importers: packages/stack-shared: dependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 '@simplewebauthn/browser': specifier: ^11.0.0 version: 11.0.0