diff --git a/apps/cloud/src/auth/context.ts b/apps/cloud/src/auth/context.ts index 6c937c5eda..d12ec24019 100644 --- a/apps/cloud/src/auth/context.ts +++ b/apps/cloud/src/auth/context.ts @@ -1,7 +1,7 @@ import { Context, Effect, Layer } from "effect"; import { makeUserStore } from "../services/user-store"; import { DbService } from "../services/db"; -import { UserStoreError } from "./errors"; +import { UserStoreError, withServiceLogging } from "./errors"; // AuthContext is defined in ./middleware.ts to keep middleware-related types together. export { AuthContext } from "./middleware"; @@ -12,24 +12,14 @@ export { AuthContext } from "./middleware"; type RawStore = ReturnType; -const makeService = (store: RawStore) => { - const use = (fn: (s: RawStore) => Promise) => - Effect.tryPromise({ - try: () => fn(store), - catch: (cause) => cause, - }).pipe( - Effect.tapError((cause) => - Effect.sync(() => { - // eslint-disable-next-line no-console - console.error("[user_store] query failed:", cause); - }), - ), - Effect.mapError(() => new UserStoreError()), - Effect.withSpan("user_store"), - ); - - return { use }; -}; +const makeService = (store: RawStore) => ({ + use: (fn: (s: RawStore) => Promise) => + withServiceLogging( + "user_store", + () => new UserStoreError(), + Effect.tryPromise({ try: () => fn(store), catch: (e) => e }), + ), +}); type UserStoreServiceType = ReturnType; diff --git a/apps/cloud/src/auth/errors.ts b/apps/cloud/src/auth/errors.ts index 43a17139b0..51816503c1 100644 --- a/apps/cloud/src/auth/errors.ts +++ b/apps/cloud/src/auth/errors.ts @@ -1,10 +1,6 @@ import { HttpApiSchema } from "@effect/platform"; -import { Schema } from "effect"; +import { Effect, Schema } from "effect"; -// Tagged errors returned to HTTP clients. The response payload must NEVER -// include internal details (SQL, stack traces, WorkOS error bodies) — those -// can leak schema + secrets. Keep the schema empty and log the `cause` -// server-side via Effect's logger when the error is constructed. export class UserStoreError extends Schema.TaggedError()( "UserStoreError", {}, @@ -16,3 +12,26 @@ export class WorkOSError extends Schema.TaggedError()( {}, HttpApiSchema.annotations({ status: 500 }), ) {} + +/** + * Service-boundary error wrapper. Logs the full Cause chain (drizzle + * query/params, pg error codes, nested Error.cause, etc.) via Effect's + * structured logger, then maps to a tagged error so the HTTP wire + * response contains only safe fields. + * + * Use this whenever a Promise-based API gets lifted into an Effect and + * its failure needs both debuggable server-side logging and a safe + * public shape. + */ +export const withServiceLogging = ( + name: string, + publicError: () => E, + effect: Effect.Effect, +): Effect.Effect => + effect.pipe( + Effect.tapErrorCause((cause) => + Effect.logError(`${name} failed`, cause), + ), + Effect.mapError(publicError), + Effect.withSpan(name), + ); diff --git a/apps/cloud/src/auth/workos.ts b/apps/cloud/src/auth/workos.ts index 58c6fa8c69..fc4d217d0b 100644 --- a/apps/cloud/src/auth/workos.ts +++ b/apps/cloud/src/auth/workos.ts @@ -4,7 +4,7 @@ import { Context, Effect, Layer } from "effect"; import { WorkOS } from "@workos-inc/node/worker"; -import { WorkOSError } from "./errors"; +import { WorkOSError, withServiceLogging } from "./errors"; import { server } from "../env"; const COOKIE_NAME = "wos-session"; @@ -26,18 +26,10 @@ const make = Effect.gen(function* () { const workos = new WorkOS({ apiKey, clientId }); const use = (fn: (wos: WorkOS) => Promise) => - Effect.tryPromise({ - try: () => fn(workos), - catch: (cause) => cause, - }).pipe( - Effect.tapError((cause) => - Effect.sync(() => { - // eslint-disable-next-line no-console - console.error("[workos] call failed:", cause); - }), - ), - Effect.mapError(() => new WorkOSError()), - Effect.withSpan("workos"), + withServiceLogging( + "workos", + () => new WorkOSError(), + Effect.tryPromise({ try: () => fn(workos), catch: (e) => e }), ); const authenticateSealedSession = (sessionData: string) => diff --git a/packages/core/sdk/src/errors.ts b/packages/core/sdk/src/errors.ts index c924055466..5596424e22 100644 --- a/packages/core/sdk/src/errors.ts +++ b/packages/core/sdk/src/errors.ts @@ -1,4 +1,4 @@ -import { Schema } from "effect"; +import { Data, Schema } from "effect"; import { ToolId, SecretId, PolicyId } from "./ids"; @@ -7,14 +7,13 @@ export class ToolNotFoundError extends Schema.TaggedError()( { toolId: ToolId }, ) {} -export class ToolInvocationError extends Schema.TaggedError()( +export class ToolInvocationError extends Data.TaggedError( "ToolInvocationError", - { - toolId: ToolId, - message: Schema.String, - cause: Schema.optional(Schema.Unknown), - }, -) {} +)<{ + readonly toolId: ToolId; + readonly message: string; + readonly cause?: unknown; +}> {} export class SecretNotFoundError extends Schema.TaggedError()( "SecretNotFoundError", diff --git a/packages/plugins/google-discovery/src/sdk/document.ts b/packages/plugins/google-discovery/src/sdk/document.ts index 654e2e8914..65fd1b0965 100644 --- a/packages/plugins/google-discovery/src/sdk/document.ts +++ b/packages/plugins/google-discovery/src/sdk/document.ts @@ -140,8 +140,8 @@ const DiscoveryDocumentModel = Schema.Struct({ }); type DiscoveryDocument = typeof DiscoveryDocumentModel.Type; -const toParseError = (message: string, error: unknown) => - new GoogleDiscoveryParseError({ message, error }); +const toParseError = (message: string, cause: unknown) => + new GoogleDiscoveryParseError({ message, cause }); const decodeUnknownWith = ( message: string, @@ -436,7 +436,6 @@ const manifestMethodFromMethod = (input: { if (!method.httpMethod) { return yield* new GoogleDiscoveryParseError({ message: `Google Discovery method '${methodId}' is missing httpMethod`, - error: new Error(`Missing httpMethod for ${methodId}`), }); } @@ -509,7 +508,6 @@ export const extractGoogleDiscoveryManifest = Effect.fn( return yield* new GoogleDiscoveryParseError({ message: "Google Discovery document is missing one of: name, version, rootUrl", - error: new Error("Invalid document"), }); } diff --git a/packages/plugins/google-discovery/src/sdk/errors.ts b/packages/plugins/google-discovery/src/sdk/errors.ts index 14d604b596..17c6fa70be 100644 --- a/packages/plugins/google-discovery/src/sdk/errors.ts +++ b/packages/plugins/google-discovery/src/sdk/errors.ts @@ -1,21 +1,20 @@ -import { Schema } from "effect"; +import { Data, Schema } from "effect"; +import type { Option } from "effect"; -export class GoogleDiscoveryParseError extends Schema.TaggedError()( +export class GoogleDiscoveryParseError extends Data.TaggedError( "GoogleDiscoveryParseError", - { - message: Schema.String, - error: Schema.Defect, - }, -) {} +)<{ + readonly message: string; + readonly cause?: unknown; +}> {} -export class GoogleDiscoveryInvocationError extends Schema.TaggedError()( +export class GoogleDiscoveryInvocationError extends Data.TaggedError( "GoogleDiscoveryInvocationError", - { - message: Schema.String, - statusCode: Schema.optionalWith(Schema.Number, { as: "Option" }), - error: Schema.Defect, - }, -) {} +)<{ + readonly message: string; + readonly statusCode: Option.Option; + readonly cause?: unknown; +}> {} export class GoogleDiscoveryOAuthError extends Schema.TaggedError()( "GoogleDiscoveryOAuthError", diff --git a/packages/plugins/google-discovery/src/sdk/invoke.ts b/packages/plugins/google-discovery/src/sdk/invoke.ts index 61134cdfff..f4066195bc 100644 --- a/packages/plugins/google-discovery/src/sdk/invoke.ts +++ b/packages/plugins/google-discovery/src/sdk/invoke.ts @@ -277,7 +277,6 @@ const invoke = Effect.fn("GoogleDiscovery.invoke")(function* (input: { return yield* new GoogleDiscoveryInvocationError({ message: `Missing required ${parameter.location} parameter: ${parameter.name}`, statusCode: Option.none(), - error: undefined, }); } continue; @@ -327,7 +326,7 @@ const invoke = Effect.fn("GoogleDiscovery.invoke")(function* (input: { new GoogleDiscoveryInvocationError({ message: `HTTP request failed: ${err.message}`, statusCode: Option.none(), - error: err, + cause: err, }), ), ); diff --git a/packages/plugins/graphql/src/sdk/errors.ts b/packages/plugins/graphql/src/sdk/errors.ts index 8b3e888797..ec2965c3ec 100644 --- a/packages/plugins/graphql/src/sdk/errors.ts +++ b/packages/plugins/graphql/src/sdk/errors.ts @@ -1,10 +1,10 @@ -import { Schema } from "effect"; +import { Data, Schema } from "effect"; +import type { Option } from "effect"; export class GraphqlIntrospectionError extends Schema.TaggedError()( "GraphqlIntrospectionError", { message: Schema.String, - error: Schema.Defect, }, ) {} @@ -15,11 +15,10 @@ export class GraphqlExtractionError extends Schema.TaggedError()( +export class GraphqlInvocationError extends Data.TaggedError( "GraphqlInvocationError", - { - message: Schema.String, - statusCode: Schema.optionalWith(Schema.Number, { as: "Option" }), - error: Schema.Defect, - }, -) {} +)<{ + readonly message: string; + readonly statusCode: Option.Option; + readonly cause?: unknown; +}> {} diff --git a/packages/plugins/graphql/src/sdk/introspect.ts b/packages/plugins/graphql/src/sdk/introspect.ts index 15fa665b52..62c3667f24 100644 --- a/packages/plugins/graphql/src/sdk/introspect.ts +++ b/packages/plugins/graphql/src/sdk/introspect.ts @@ -149,29 +149,31 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( } const response = yield* client.execute(request).pipe( + Effect.tapErrorCause((cause) => + Effect.logError("graphql introspection request failed", cause), + ), Effect.mapError( (err) => new GraphqlIntrospectionError({ message: `Failed to reach GraphQL endpoint: ${err.message}`, - error: err, }), ), ); if (response.status !== 200) { - const body = yield* response.text.pipe(Effect.catchAll(() => Effect.succeed(""))); return yield* new GraphqlIntrospectionError({ - message: `Introspection failed with status ${response.status}: ${body}`, - error: undefined, + message: `Introspection failed with status ${response.status}`, }); } const raw = yield* response.json.pipe( + Effect.tapErrorCause((cause) => + Effect.logError("graphql introspection JSON parse failed", cause), + ), Effect.mapError( - (err) => + () => new GraphqlIntrospectionError({ message: `Failed to parse introspection response as JSON`, - error: err, }), ), ); @@ -180,16 +182,13 @@ export const introspect = Effect.fn("GraphQL.introspect")(function* ( if (json.errors && Array.isArray(json.errors) && json.errors.length > 0) { return yield* new GraphqlIntrospectionError({ - // @effect-diagnostics-next-line preferSchemaOverJson:off - message: `Introspection returned errors: ${JSON.stringify(json.errors)}`, - error: undefined, + message: `Introspection returned ${json.errors.length} error(s)`, }); } if (!json.data?.__schema) { return yield* new GraphqlIntrospectionError({ message: "Introspection response missing __schema", - error: undefined, }); } @@ -216,6 +215,5 @@ export const parseIntrospectionJson = ( catch: (err) => new GraphqlIntrospectionError({ message: `Failed to parse introspection JSON: ${err instanceof Error ? err.message : String(err)}`, - error: err, }), }); diff --git a/packages/plugins/graphql/src/sdk/invoke.ts b/packages/plugins/graphql/src/sdk/invoke.ts index 00bd4cf078..7ae4a2a3d2 100644 --- a/packages/plugins/graphql/src/sdk/invoke.ts +++ b/packages/plugins/graphql/src/sdk/invoke.ts @@ -109,7 +109,7 @@ export const invoke = Effect.fn("GraphQL.invoke")(function* ( new GraphqlInvocationError({ message: `GraphQL request failed: ${err.message}`, statusCode: Option.none(), - error: err, + cause: err, }), ), ); diff --git a/packages/plugins/keychain/src/errors.ts b/packages/plugins/keychain/src/errors.ts index 3e0eaaf4b3..53730ac0b3 100644 --- a/packages/plugins/keychain/src/errors.ts +++ b/packages/plugins/keychain/src/errors.ts @@ -1,9 +1,6 @@ -import { Schema } from "effect"; +import { Data } from "effect"; -export class KeychainError extends Schema.TaggedError()( - "KeychainError", - { - message: Schema.String, - cause: Schema.optional(Schema.Unknown), - }, -) {} +export class KeychainError extends Data.TaggedError("KeychainError")<{ + readonly message: string; + readonly cause?: unknown; +}> {} diff --git a/packages/plugins/openapi/src/sdk/errors.ts b/packages/plugins/openapi/src/sdk/errors.ts index 2c46d558dd..46478c7cbc 100644 --- a/packages/plugins/openapi/src/sdk/errors.ts +++ b/packages/plugins/openapi/src/sdk/errors.ts @@ -1,10 +1,10 @@ -import { Schema } from "effect"; +import { Data, Schema } from "effect"; +import type { Option } from "effect"; export class OpenApiParseError extends Schema.TaggedError()( "OpenApiParseError", { message: Schema.String, - error: Schema.Defect, }, ) {} @@ -15,11 +15,10 @@ export class OpenApiExtractionError extends Schema.TaggedError()( +export class OpenApiInvocationError extends Data.TaggedError( "OpenApiInvocationError", - { - message: Schema.String, - statusCode: Schema.optionalWith(Schema.Number, { as: "Option" }), - error: Schema.Defect, - }, -) {} +)<{ + readonly message: string; + readonly statusCode: Option.Option; + readonly cause?: unknown; +}> {} diff --git a/packages/plugins/openapi/src/sdk/invoke.ts b/packages/plugins/openapi/src/sdk/invoke.ts index 82969fb451..0c45bcfb43 100644 --- a/packages/plugins/openapi/src/sdk/invoke.ts +++ b/packages/plugins/openapi/src/sdk/invoke.ts @@ -76,7 +76,6 @@ const resolvePath = Effect.fn("OpenApi.resolvePath")(function* ( return yield* new OpenApiInvocationError({ message: `Missing required path parameter: ${param.name}`, statusCode: Option.none(), - error: undefined, }); } continue; @@ -111,7 +110,6 @@ const resolvePath = Effect.fn("OpenApi.resolvePath")(function* ( return yield* new OpenApiInvocationError({ message: `Unresolved path parameters: ${[...new Set(unresolved)].join(", ")}`, statusCode: Option.none(), - error: undefined, }); } @@ -245,7 +243,7 @@ export const invoke = Effect.fn("OpenApi.invoke")(function* ( new OpenApiInvocationError({ message: `HTTP request failed: ${err.message}`, statusCode: Option.none(), - error: err, + cause: err, }), ), ); diff --git a/packages/plugins/openapi/src/sdk/parse.ts b/packages/plugins/openapi/src/sdk/parse.ts index f97719cdfc..115c07978c 100644 --- a/packages/plugins/openapi/src/sdk/parse.ts +++ b/packages/plugins/openapi/src/sdk/parse.ts @@ -20,7 +20,6 @@ export const parse = Effect.fn("OpenApi.parse")(function* (input: string) { catch: (error) => new OpenApiParseError({ message: `Failed to parse OpenAPI document: ${error instanceof Error ? error.message : String(error)}`, - error, }), });