From b951d71f9ce86861ca8983f7e764ccaee628faa0 Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:51:13 +0100 Subject: [PATCH 01/53] chore(logging): operations logging around access request mutations --- .../accessrequests/graph/resolvers/index.ts | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/packages/server/modules/accessrequests/graph/resolvers/index.ts b/packages/server/modules/accessrequests/graph/resolvers/index.ts index 5975cb751..eeea29548 100644 --- a/packages/server/modules/accessrequests/graph/resolvers/index.ts +++ b/packages/server/modules/accessrequests/graph/resolvers/index.ts @@ -31,6 +31,7 @@ import { import { authorizeResolver } from '@/modules/shared' import { LogicError } from '@/modules/shared/errors' import { getEventBus } from '@/modules/shared/services/eventBus' +import { withOperationLogging } from '@/observability/domain/businessLogging' const getUser = getUserFactory({ db }) const getStream = getStreamFactory({ db }) @@ -103,7 +104,20 @@ const resolvers: Resolvers = { if (!userId) throw new LogicError('User ID unexpectedly false') const { streamId } = args - return await requestStreamAccess(userId, streamId) + const logger = ctx.log.child({ + streamId, + projectId: streamId + }) + const result = await withOperationLogging( + async () => await requestStreamAccess(userId, streamId), + { + logger, + operationName: 'requestStreamAccess', + operationDescription: 'Request for stream access' + } + ) + if (!result) throw new LogicError('Unable to create stream access request') // should have already thrown by this point + return result } }, ProjectMutations: { @@ -113,25 +127,50 @@ const resolvers: Resolvers = { async create(_parent, args, ctx) { const { userId } = ctx const { projectId } = args - return await requestProjectAccess(userId!, projectId) + const logger = ctx.log.child({ + projectId, + streamId: projectId // for legacy compatibility + }) + const result = await withOperationLogging( + async () => await requestProjectAccess(userId!, projectId), + { + logger, + operationName: 'CreateProjectAccessRequest', + operationDescription: 'Create a request for project access' + } + ) + if (!result) throw new LogicError('Unable to create project access request') // should have already thrown by this point + return result }, async use(_parent, args, ctx) { const { userId, resourceAccessRules } = ctx const { requestId, accept, role } = args + const logger = ctx.log - const usedReq = await processPendingProjectRequest( - userId!, - requestId, - accept, - mapStreamRoleToValue(role), - resourceAccessRules + const project = await withOperationLogging( + async () => { + const usedReq = await processPendingProjectRequest( + userId!, + requestId, + accept, + mapStreamRoleToValue(role), + resourceAccessRules + ) + + const project = await ctx.loaders.streams.getStream.load(usedReq.resourceId) + if (!project) { + throw new LogicError('Unexpectedly unable to find request project') + } + + return project + }, + { + logger, + operationName: 'ProcessProjectAccessRequest', + operationDescription: 'Use a request for project access' + } ) - - const project = await ctx.loaders.streams.getStream.load(usedReq.resourceId) - if (!project) { - throw new LogicError('Unexpectedly unable to find request project') - } - + if (!project) throw new LogicError('Unable to user project access request') // should have already thrown by this point return project } }, From c937c4c30cedcb6885e77de20a790de2ae016bab Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:49:36 +0100 Subject: [PATCH 02/53] Improve error handling --- .../accessrequests/graph/resolvers/index.ts | 30 ++++++++----------- .../server/modules/gatekeeper/rest/billing.ts | 2 +- .../observability/domain/businessLogging.ts | 6 ++-- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/packages/server/modules/accessrequests/graph/resolvers/index.ts b/packages/server/modules/accessrequests/graph/resolvers/index.ts index eeea29548..07825f5b8 100644 --- a/packages/server/modules/accessrequests/graph/resolvers/index.ts +++ b/packages/server/modules/accessrequests/graph/resolvers/index.ts @@ -108,7 +108,7 @@ const resolvers: Resolvers = { streamId, projectId: streamId }) - const result = await withOperationLogging( + return await withOperationLogging( async () => await requestStreamAccess(userId, streamId), { logger, @@ -116,8 +116,6 @@ const resolvers: Resolvers = { operationDescription: 'Request for stream access' } ) - if (!result) throw new LogicError('Unable to create stream access request') // should have already thrown by this point - return result } }, ProjectMutations: { @@ -131,7 +129,7 @@ const resolvers: Resolvers = { projectId, streamId: projectId // for legacy compatibility }) - const result = await withOperationLogging( + return await withOperationLogging( async () => await requestProjectAccess(userId!, projectId), { logger, @@ -139,38 +137,34 @@ const resolvers: Resolvers = { operationDescription: 'Create a request for project access' } ) - if (!result) throw new LogicError('Unable to create project access request') // should have already thrown by this point - return result }, async use(_parent, args, ctx) { const { userId, resourceAccessRules } = ctx const { requestId, accept, role } = args const logger = ctx.log - const project = await withOperationLogging( - async () => { - const usedReq = await processPendingProjectRequest( + const usedReq = await withOperationLogging( + async () => + await processPendingProjectRequest( userId!, requestId, accept, mapStreamRoleToValue(role), resourceAccessRules - ) + ), - const project = await ctx.loaders.streams.getStream.load(usedReq.resourceId) - if (!project) { - throw new LogicError('Unexpectedly unable to find request project') - } - - return project - }, { logger, operationName: 'ProcessProjectAccessRequest', operationDescription: 'Use a request for project access' } ) - if (!project) throw new LogicError('Unable to user project access request') // should have already thrown by this point + + const project = await ctx.loaders.streams.getStream.load(usedReq.resourceId) + if (!project) { + throw new LogicError('Unexpectedly unable to find request project') + } + return project } }, diff --git a/packages/server/modules/gatekeeper/rest/billing.ts b/packages/server/modules/gatekeeper/rest/billing.ts index 6754fa96e..f048386ad 100644 --- a/packages/server/modules/gatekeeper/rest/billing.ts +++ b/packages/server/modules/gatekeeper/rest/billing.ts @@ -153,7 +153,7 @@ export const getBillingRouter = (): Router => { operationName: 'completeCheckoutSession', operationDescription: 'Payment succeeded or Stripe session completed, and payment was paid', - errorHandler: (err, logger) => { + errorHandler: async (err, logger) => { if (err instanceof WorkspaceAlreadyPaidError) { // ignore the request, this is prob a replay from stripe logger.info('Workspace is already paid, ignoring') diff --git a/packages/server/observability/domain/businessLogging.ts b/packages/server/observability/domain/businessLogging.ts index 145f1f52a..1e545bc84 100644 --- a/packages/server/observability/domain/businessLogging.ts +++ b/packages/server/observability/domain/businessLogging.ts @@ -18,9 +18,9 @@ export const withOperationLogging = async ( logger: Logger operationName: string operationDescription?: string - errorHandler?: (err: unknown, logger: Logger) => MaybeAsync + errorHandler?: (err: unknown, logger: Logger) => MaybeAsync } -): Promise => { +): Promise => { const { operationName, operationDescription } = params const errorHandler = params.errorHandler || logErrorThenThrow const logger = params.logger.child(OperationName(operationName)) @@ -36,6 +36,6 @@ export const withOperationLogging = async ( logger.info(OperationStatus.success, OperationLogLinePrefix) return results } catch (err) { - await errorHandler(err, logger) + return await errorHandler(err, logger) } } From 1c17d601d83a1cffac3d33df697b2e1e3a28beae Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:07:06 +0100 Subject: [PATCH 03/53] feat(server/logging): add operations logging to comment mutations --- .../comments/graph/resolvers/comments.ts | 179 ++++++++++++++---- .../observability/domain/businessLogging.ts | 6 +- 2 files changed, 143 insertions(+), 42 deletions(-) diff --git a/packages/server/modules/comments/graph/resolvers/comments.ts b/packages/server/modules/comments/graph/resolvers/comments.ts index ac114092f..b278e6c90 100644 --- a/packages/server/modules/comments/graph/resolvers/comments.ts +++ b/packages/server/modules/comments/graph/resolvers/comments.ts @@ -91,6 +91,7 @@ import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' import { Knex } from 'knex' import { getEventBus } from '@/modules/shared/services/eventBus' import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper' +import { withOperationLogging } from '@/observability/domain/businessLogging' // We can use the main DB for these const getStream = getStreamFactory({ db }) @@ -495,6 +496,11 @@ export = { }) throwIfAuthNotOk(canCreate) + const logger = ctx.log.child({ + projectId, + streamId: projectId //legacy + }) + const projectDb = await getProjectDbClient({ projectId }) const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({ @@ -517,16 +523,28 @@ export = { emitEvent: getEventBus().emit }) - return await createCommentThreadAndNotify(args.input, ctx.userId!) + return await withOperationLogging( + async () => await createCommentThreadAndNotify(args.input, ctx.userId!), + { + operationName: 'createCommentThread', + operationDescription: 'Create comment thread', + logger + } + ) }, async reply(_parent, args, ctx) { + const projectId = args.input.projectId const canCreateComment = await ctx.authPolicies.project.comment.canCreate({ userId: ctx.userId, - projectId: args.input.projectId + projectId }) throwIfAuthNotOk(canCreateComment) + const logger = ctx.log.child({ + projectId, + streamId: projectId //legacy + }) - const projectDb = await getProjectDbClient({ projectId: args.input.projectId }) + const projectDb = await getProjectDbClient({ projectId }) const getComment = getCommentFactory({ db: projectDb }) const validateInputAttachments = validateInputAttachmentsFactory({ getBlobs: getBlobsFactory({ db: projectDb }) @@ -549,18 +567,33 @@ export = { }) }) - return await createCommentReplyAndNotify(args.input, ctx.userId!) + return await withOperationLogging( + async () => await createCommentReplyAndNotify(args.input, ctx.userId!), + { + operationName: 'replyToComment', + operationDescription: 'Reply to comment', + logger + } + ) }, async edit(_parent, args, ctx) { + const projectId = args.input.projectId + const commentId = args.input.commentId const canEditComment = await ctx.authPolicies.project.comment.canEdit({ - projectId: args.input.projectId, + projectId, userId: ctx.userId, - commentId: args.input.commentId + commentId }) throwIfAuthNotOk(canEditComment) + const logger = ctx.log.child({ + projectId, + streamId: projectId, //legacy + commentId + }) + const projectDb = await getProjectDbClient({ - projectId: args.input.projectId + projectId }) const getComment = getCommentFactory({ db: projectDb }) const validateInputAttachments = validateInputAttachmentsFactory({ @@ -575,18 +608,28 @@ export = { emitEvent: getEventBus().emit }) - return await editCommentAndNotify(args.input, ctx.userId!) + return await withOperationLogging( + async () => await editCommentAndNotify(args.input, ctx.userId!), + { logger, operationName: 'editComment', operationDescription: 'Edit comment' } + ) }, async archive(_parent, args, ctx) { + const projectId = args.input.projectId + const commentId = args.input.commentId const canArchive = await ctx.authPolicies.project.comment.canArchive({ userId: ctx.userId, - projectId: args.input.projectId, - commentId: args.input.commentId + projectId, + commentId }) throwIfAuthNotOk(canArchive) + const logger = ctx.log.child({ + projectId, + streamId: projectId, //legacy + commentId + }) const projectDb = await getProjectDbClient({ - projectId: args.input.projectId + projectId }) const getComment = getCommentFactory({ db: projectDb }) const getStream = getStreamFactory({ db: projectDb }) @@ -605,10 +648,14 @@ export = { emitEvent: getEventBus().emit }) - await archiveCommentAndNotify( - args.input.commentId, - ctx.userId!, - args.input.archived + await withOperationLogging( + async () => + await archiveCommentAndNotify(commentId, ctx.userId!, args.input.archived), + { + logger, + operationName: 'archiveComment', + operationDescription: 'Archive comment' + } ) return true } @@ -675,13 +722,19 @@ export = { }, async commentCreate(_parent, args, context) { + const projectId = args.input.streamId const canCreate = await context.authPolicies.project.comment.canCreate({ userId: context.userId, - projectId: args.input.streamId + projectId }) throwIfAuthNotOk(canCreate) - const projectDb = await getProjectDbClient({ projectId: args.input.streamId }) + const logger = context.log.child({ + projectId, + streamId: projectId //legacy + }) + + const projectDb = await getProjectDbClient({ projectId }) const getViewerResourcesFromLegacyIdentifiers = buildGetViewerResourcesFromLegacyIdentifiers({ db: projectDb }) @@ -699,23 +752,39 @@ export = { emitEvent: getEventBus().emit, getViewerResourcesFromLegacyIdentifiers }) - const comment = await createComment({ - userId: context.userId!, - input: args.input - }) + const comment = await withOperationLogging( + async () => + await createComment({ + userId: context.userId!, + input: args.input + }), + { + operationName: 'createComment', + operationDescription: 'Create comment', + logger + } + ) return comment.id }, async commentEdit(_parent, args, context) { + const projectId = args.input.streamId + const commentId = args.input.id const canEdit = await context.authPolicies.project.comment.canEdit({ userId: context.userId, - projectId: args.input.streamId, - commentId: args.input.id + projectId, + commentId }) throwIfAuthNotOk(canEdit) - const projectDb = await getProjectDbClient({ projectId: args.input.streamId }) + const logger = context.log.child({ + projectId, + streamId: projectId, //legacy + commentId + }) + + const projectDb = await getProjectDbClient({ projectId }) const editComment = editCommentFactory({ getComment: getCommentFactory({ db: projectDb }), validateInputAttachments: validateInputAttachmentsFactory({ @@ -725,7 +794,10 @@ export = { emitEvent: getEventBus().emit }) - await editComment({ userId: context.userId!, input: args.input }) + await withOperationLogging( + async () => await editComment({ userId: context.userId!, input: args.input }), + { operationName: 'editComment', operationDescription: 'Edit comment', logger } + ) return true }, @@ -745,38 +817,59 @@ export = { }, async commentArchive(_parent, args, context) { + const projectId = args.streamId + const commentId = args.commentId const canArchive = await context.authPolicies.project.comment.canArchive({ userId: context.userId, - projectId: args.streamId, - commentId: args.commentId + projectId, + commentId }) throwIfAuthNotOk(canArchive) - const projectDb = await getProjectDbClient({ projectId: args.streamId }) + const logger = context.log.child({ + projectId, + streamId: projectId, //legacy + commentId + }) + + const projectDb = await getProjectDbClient({ projectId }) const archiveComment = archiveCommentFactory({ getComment: getCommentFactory({ db: projectDb }), getStream, updateComment: updateCommentFactory({ db: projectDb }), emitEvent: getEventBus().emit }) - await archiveComment({ ...args, userId: context.userId! }) // NOTE: permissions check inside service + await withOperationLogging( + async () => await archiveComment({ ...args, userId: context.userId! }), // NOTE: permissions check inside service + { + logger, + operationName: 'archiveComment', + operationDescription: 'Archive comment' + } + ) return true }, async commentReply(_parent, args, context) { + const projectId = args.input.streamId if (!context.userId) throw new ForbiddenError('Only registered users can comment.') + const logger = context.log.child({ + projectId, + streamId: projectId //legacy + }) + const stream = await getStream({ - streamId: args.input.streamId, + streamId: projectId, userId: context.userId }) if (!stream?.allowPublicComments && !stream?.role) throw new ForbiddenError('You are not authorized.') - const projectDb = await getProjectDbClient({ projectId: args.input.streamId }) + const projectDb = await getProjectDbClient({ projectId }) const createCommentReply = createCommentReplyFactory({ validateInputAttachments: validateInputAttachmentsFactory({ @@ -796,14 +889,22 @@ export = { buildGetViewerResourcesFromLegacyIdentifiers({ db: projectDb }) }) }) - const reply = await createCommentReply({ - authorId: context.userId, - parentCommentId: args.input.parentComment, - streamId: args.input.streamId, - text: args.input.text as SmartTextEditorValueSchema, - data: args.input.data ?? null, - blobIds: args.input.blobIds - }) + const reply = await withOperationLogging( + async () => + await createCommentReply({ + authorId: context.userId, + parentCommentId: args.input.parentComment, + streamId: args.input.streamId, + text: args.input.text as SmartTextEditorValueSchema, + data: args.input.data ?? null, + blobIds: args.input.blobIds + }), + { + logger, + operationName: 'createCommentReply', + operationDescription: 'Create comment reply' + } + ) return reply.id } diff --git a/packages/server/observability/domain/businessLogging.ts b/packages/server/observability/domain/businessLogging.ts index 145f1f52a..1e545bc84 100644 --- a/packages/server/observability/domain/businessLogging.ts +++ b/packages/server/observability/domain/businessLogging.ts @@ -18,9 +18,9 @@ export const withOperationLogging = async ( logger: Logger operationName: string operationDescription?: string - errorHandler?: (err: unknown, logger: Logger) => MaybeAsync + errorHandler?: (err: unknown, logger: Logger) => MaybeAsync } -): Promise => { +): Promise => { const { operationName, operationDescription } = params const errorHandler = params.errorHandler || logErrorThenThrow const logger = params.logger.child(OperationName(operationName)) @@ -36,6 +36,6 @@ export const withOperationLogging = async ( logger.info(OperationStatus.success, OperationLogLinePrefix) return results } catch (err) { - await errorHandler(err, logger) + return await errorHandler(err, logger) } } From 7deb4554c6836ebc88ca68004b0c87ecaff64c5e Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Mon, 14 Apr 2025 17:20:07 +0100 Subject: [PATCH 04/53] chore(server/logging): add operation logs to email module --- .../modules/emails/graph/resolvers/index.ts | 18 +++++++++++++++--- packages/server/modules/emails/rest/index.ts | 13 +++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/server/modules/emails/graph/resolvers/index.ts b/packages/server/modules/emails/graph/resolvers/index.ts index cc31f05fb..848a8ce5e 100644 --- a/packages/server/modules/emails/graph/resolvers/index.ts +++ b/packages/server/modules/emails/graph/resolvers/index.ts @@ -13,6 +13,7 @@ import { import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { requestEmailVerificationFactory } from '@/modules/emails/services/verification/request' +import { withOperationLogging } from '@/observability/domain/businessLogging' const getUser = getUserFactory({ db }) const requestEmailVerification = requestEmailVerificationFactory({ @@ -38,14 +39,25 @@ export = { Mutation: { async requestVerification(_parent, _args, ctx) { const { userId } = ctx - await requestEmailVerification(userId || '') + await withOperationLogging( + async () => await requestEmailVerification(userId || ''), + { + logger: ctx.log, + operationName: 'requestEmailVerification', + operationDescription: 'Request email verification' + } + ) return true }, - async requestVerificationByEmail(_parent, args) { + async requestVerificationByEmail(_parent, args, ctx) { const { email } = args const user = await getUserByEmail(email) if (!user?.email || user.verified) return false - await requestEmailVerification(user.id) + await withOperationLogging(async () => await requestEmailVerification(user.id), { + logger: ctx.log, + operationName: 'requestEmailVerificationFromEmail', + operationDescription: `Request verification by email` + }) return true } } diff --git a/packages/server/modules/emails/rest/index.ts b/packages/server/modules/emails/rest/index.ts index b02f83910..b0d44b050 100644 --- a/packages/server/modules/emails/rest/index.ts +++ b/packages/server/modules/emails/rest/index.ts @@ -9,9 +9,11 @@ import { } from '@/modules/emails/repositories' import { db } from '@/db/knex' import { markUserAsVerifiedFactory } from '@/modules/core/repositories/users' +import { withOperationLogging } from '@/observability/domain/businessLogging' export = (app: Express) => { app.get('/auth/verifyemail', async (req, res) => { + const logger = req.log try { const finalizeEmailVerification = finalizeEmailVerificationFactory({ getPendingToken: getPendingTokenFactory({ db }), @@ -19,7 +21,14 @@ export = (app: Express) => { deleteVerifications: deleteVerificationsFactory({ db }) }) - await finalizeEmailVerification(req.query.t as Optional) + await withOperationLogging( + async () => await finalizeEmailVerification(req.query.t as Optional), + { + logger, + operationName: 'finalizeEmailVerification', + operationDescription: 'Finalize email verification' + } + ) return res.redirect( new URL('/?emailverifiedstatus=true', getFrontendOrigin()).toString() ) @@ -28,7 +37,7 @@ export = (app: Express) => { error instanceof EmailVerificationFinalizationError ? error.message : 'Email verification unexpectedly failed' - req.log.info({ err: error }, 'Email verification failed.') + logger.info({ err: error }, 'Email verification failed.') return res.redirect( new URL(`/?emailverifiederror=${msg}`, getFrontendOrigin()).toString() From 9c5c119f191e4560482f95229b4ffbf0491ecfc4 Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:18:37 +0100 Subject: [PATCH 05/53] fix bug --- packages/server/modules/comments/graph/resolvers/comments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/modules/comments/graph/resolvers/comments.ts b/packages/server/modules/comments/graph/resolvers/comments.ts index b278e6c90..eec6f13b7 100644 --- a/packages/server/modules/comments/graph/resolvers/comments.ts +++ b/packages/server/modules/comments/graph/resolvers/comments.ts @@ -892,7 +892,7 @@ export = { const reply = await withOperationLogging( async () => await createCommentReply({ - authorId: context.userId, + authorId: context.userId!, parentCommentId: args.input.parentComment, streamId: args.input.streamId, text: args.input.text as SmartTextEditorValueSchema, From 3c51f89bdbd390d1efe287bb41ad164f1d57da59 Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:29:43 +0100 Subject: [PATCH 06/53] chore(server/logging): add operations logging to fileuploads module --- .../server/modules/fileuploads/rest/routes.ts | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/server/modules/fileuploads/rest/routes.ts b/packages/server/modules/fileuploads/rest/routes.ts index d43f6b552..712a75de6 100644 --- a/packages/server/modules/fileuploads/rest/routes.ts +++ b/packages/server/modules/fileuploads/rest/routes.ts @@ -11,6 +11,7 @@ import { getRolesFactory } from '@/modules/shared/repositories/roles' import { getStreamBranchByNameFactory } from '@/modules/core/repositories/branches' import { getStreamFactory } from '@/modules/core/repositories/streams' import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' +import { withOperationLogging } from '@/observability/domain/businessLogging' export const getRouter = () => { const app = Router() @@ -73,18 +74,26 @@ export const getRouter = () => { `http://127.0.0.1:${getPort()}/api/stream/${req.params.streamId}/blob`, async (err, response, body) => { if (err) { - res.log.error(err, 'Error while uploading blob.') + req.log.error(err, 'Error while uploading blob.') res.status(500).send(err.message) return } if (response.statusCode === 201) { const { uploadResults } = JSON.parse(body) - await saveFileUploads({ - userId: req.context.userId!, - streamId: req.params.streamId, - branchName, - uploadResults - }) + await withOperationLogging( + async () => + await saveFileUploads({ + userId: req.context.userId!, + streamId: req.params.streamId, + branchName, + uploadResults + }), + { + logger: req.log, + operationName: 'uploadFile', + operationDescription: 'Upload a file to a stream' + } + ) } else { res.log.error( { From 861092c93b2466164cb98fd8fc48e3efd15ae532 Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:36:48 +0100 Subject: [PATCH 07/53] chore(server/logging): operations logging for gendo module --- .../modules/gendo/graph/resolvers/index.ts | 26 ++++++++++++++----- packages/server/modules/gendo/rest/index.ts | 26 +++++++++++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/server/modules/gendo/graph/resolvers/index.ts b/packages/server/modules/gendo/graph/resolvers/index.ts index 19b9681c4..ddfb81c24 100644 --- a/packages/server/modules/gendo/graph/resolvers/index.ts +++ b/packages/server/modules/gendo/graph/resolvers/index.ts @@ -40,6 +40,7 @@ import { } from '@/modules/shared/helpers/envHelper' import { getProjectObjectStorage } from '@/modules/multiregion/utils/blobStorageSelector' import { storeFileStreamFactory } from '@/modules/blobstorage/repositories/blobs' +import { withOperationLogging } from '@/observability/domain/businessLogging' const upsertUserCredits = upsertUserCreditsFactory({ db }) const getUserGendoAiCredits = getUserGendoAiCreditsFactory({ @@ -79,6 +80,7 @@ export = FF_GENDOAI_MODULE_ENABLED }, VersionMutations: { async requestGendoAIRender(__parent, args, ctx) { + const projectId = args.input.projectId const rateLimitResult = await getRateLimitResult( 'GENDO_AI_RENDER_REQUEST', ctx.userId as string @@ -89,14 +91,18 @@ export = FF_GENDOAI_MODULE_ENABLED await authorizeResolver( ctx.userId, - args.input.projectId, + projectId, Roles.Stream.Reviewer, ctx.resourceAccessRules ) + const logger = ctx.log.child({ + projectId, + streamId: projectId //legacy + }) + const userId = ctx.userId! - const projectId = args.input.projectId const [projectDb, projectStorage] = await Promise.all([ getProjectDbClient({ projectId @@ -128,10 +134,18 @@ export = FF_GENDOAI_MODULE_ENABLED publish }) - await createRenderRequest({ - ...args.input, - userId - }) + await withOperationLogging( + async () => + await createRenderRequest({ + ...args.input, + userId + }), + { + logger, + operationName: 'createGendoRenderRequest', + operationDescription: 'Request GendoAI to generate a render' + } + ) return true } diff --git a/packages/server/modules/gendo/rest/index.ts b/packages/server/modules/gendo/rest/index.ts index d894fa68d..20251890a 100644 --- a/packages/server/modules/gendo/rest/index.ts +++ b/packages/server/modules/gendo/rest/index.ts @@ -16,6 +16,7 @@ import { createHmac, timingSafeEqual } from 'node:crypto' import { getGendoAIKey } from '@/modules/shared/helpers/envHelper' import { getProjectObjectStorage } from '@/modules/multiregion/utils/blobStorageSelector' import { storeFileStreamFactory } from '@/modules/blobstorage/repositories/blobs' +import { withOperationLogging } from '@/observability/domain/businessLogging' export default function (app: express.Express) { // const responseToken = getGendoAIResponseKey() @@ -47,6 +48,12 @@ export default function (app: express.Express) { const gendoGenerationId = payload.generationId const projectId = req.params.projectId + const logger = req.log.child({ + projectId, + gendoGenerationId, + gendoResponseStatus: status + }) + const [projectDb, projectStorage] = await Promise.all([ getProjectDbClient({ projectId }), getProjectObjectStorage({ projectId }) @@ -64,11 +71,20 @@ export default function (app: express.Express) { publish }) - await updateRenderRequest({ - gendoGenerationId, - responseImage, - status - }) + await withOperationLogging( + async () => + await updateRenderRequest({ + gendoGenerationId, + responseImage, + status + }), + { + logger, + operationName: 'updateGendoRenderRequest', + operationDescription: + 'Handle response from GendoAI and update a render request' + } + ) res.status(200).send('Speckle says thank you 💖') } From 146192baea055b6b55ce62851fe532f97ffaab54 Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Mon, 14 Apr 2025 21:39:58 +0100 Subject: [PATCH 08/53] Re-add seat upgrade icons --- .../components/billing/TransitionCards.vue | 2 +- .../members/actions/SeatTransitionCards.vue | 29 +++++++++++---- .../members/actions/UpdateAdminDialog.vue | 35 +++---------------- 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/packages/frontend-2/components/billing/TransitionCards.vue b/packages/frontend-2/components/billing/TransitionCards.vue index 01197b935..a74116366 100644 --- a/packages/frontend-2/components/billing/TransitionCards.vue +++ b/packages/frontend-2/components/billing/TransitionCards.vue @@ -12,7 +12,7 @@ diff --git a/packages/frontend-2/components/settings/workspaces/members/actions/SeatTransitionCards.vue b/packages/frontend-2/components/settings/workspaces/members/actions/SeatTransitionCards.vue index 492c49d28..59dd81d2d 100644 --- a/packages/frontend-2/components/settings/workspaces/members/actions/SeatTransitionCards.vue +++ b/packages/frontend-2/components/settings/workspaces/members/actions/SeatTransitionCards.vue @@ -2,17 +2,34 @@ - -

- {{ roleInfo }} Learn more about - - workspace roles. - -

- -

- Note that the Editor seat is a paid seat type if your workspace is subscribed to - one of the paid plans. -

diff --git a/packages/frontend-2/components/settings/workspaces/billing/addOns/AddOns.vue b/packages/frontend-2/components/settings/workspaces/billing/addOns/AddOns.vue index bf1ab400f..fda3e5944 100644 --- a/packages/frontend-2/components/settings/workspaces/billing/addOns/AddOns.vue +++ b/packages/frontend-2/components/settings/workspaces/billing/addOns/AddOns.vue @@ -59,6 +59,7 @@ import { useWorkspaceAddonPrices } from '~/lib/billing/composables/prices' import { formatPrice } from '~/lib/billing/helpers/plan' import { PaidWorkspacePlansNew, type MaybeNullOrUndefined } from '@speckle/shared' import { BillingInterval } from '~/lib/common/generated/gql/graphql' +import { useActiveWorkspace } from '~/lib/workspaces/composables/activeWorkspace' const props = defineProps<{ slug: string @@ -77,6 +78,7 @@ const { hasUnlimitedAddon } = useWorkspacePlan(props.slug) const { addonPrices } = useWorkspaceAddonPrices() +const { isAdmin } = useActiveWorkspace(props.slug) const isUpgradeDialogOpen = ref(false) @@ -95,7 +97,8 @@ const unlimitedAddOnButton = computed(() => ({ disabled: !isPaidPlan.value || (!isYearlyIntervalSelected.value && intervalIsYearly.value) || - hasUnlimitedAddon.value, + hasUnlimitedAddon.value || + !isAdmin.value, onClick: () => { isUpgradeDialogOpen.value = true } diff --git a/packages/frontend-2/components/settings/workspaces/billing/upgradeDialog/Summary.vue b/packages/frontend-2/components/settings/workspaces/billing/upgradeDialog/Summary.vue index c4084cd37..bd4327fdc 100644 --- a/packages/frontend-2/components/settings/workspaces/billing/upgradeDialog/Summary.vue +++ b/packages/frontend-2/components/settings/workspaces/billing/upgradeDialog/Summary.vue @@ -4,11 +4,16 @@