Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 48 additions & 48 deletions sentry.client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,55 @@
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import * as Sentry from "@sentry/nextjs";
import * as Sentry from '@sentry/nextjs'

const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN;
const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV;
const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN
const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV
const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'

if (dsn) {
Sentry.init({
dsn,

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: isProd ? 0.2 : 1,
profilesSampleRate: 0.1,
// NOTE: reducing sample only 10% of transactions in prod to get general trends instead of detailed and overfitted data

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

// You can remove this option if you're not planning to use the Sentry Session Replay feature:
// NOTE: Since session replay barely helps us anyways, getting rid of it to reduce some bundle size at least
// replaysOnErrorSampleRate: 1.0,
// replaysSessionSampleRate: 0,
integrations: [
Sentry.browserTracingIntegration({
beforeStartSpan: (e) => {
console.info("SentryBrowserTracingSpan", e.name);
return e;
},
}),
// Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
// maskAllText: true,
// blockAllMedia: true,
// }),
],

// ignoreErrors: [/fetch failed/i],
ignoreErrors: [/fetch failed/i],

beforeSend(event) {
if (!isProd && event.type === undefined) {
return null;
}
event.tags = {
...event.tags,
// Adding additional app_env tag for cross-checking
app_env: isProd ? "production" : vercelEnv || "development",
};
return event;
},
});
Sentry.init({
dsn,

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: isProd ? 0.2 : 1,
profilesSampleRate: 0.1,
// NOTE: reducing sample only 10% of transactions in prod to get general trends instead of detailed and overfitted data

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

// You can remove this option if you're not planning to use the Sentry Session Replay feature:
// NOTE: Since session replay barely helps us anyways, getting rid of it to reduce some bundle size at least
// replaysOnErrorSampleRate: 1.0,
// replaysSessionSampleRate: 0,
integrations: [
Sentry.browserTracingIntegration({
beforeStartSpan: (e) => {
console.info('SentryBrowserTracingSpan', e.name)
return e
},
}),
// Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
// maskAllText: true,
// blockAllMedia: true,
// }),
],

// ignoreErrors: [/fetch failed/i],
ignoreErrors: [/fetch failed/i],

beforeSend(event) {
if (!isProd && event.type === undefined) {
return null
}
event.tags = {
...event.tags,
// Adding additional app_env tag for cross-checking
app_env: isProd ? 'production' : vercelEnv || 'development',
}
return event
},
})
}
40 changes: 20 additions & 20 deletions sentry.server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import * as Sentry from "@sentry/nextjs";
import * as Sentry from '@sentry/nextjs'

const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN;
const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV;
const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN
const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV
const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production'

if (dsn) {
Sentry.init({
dsn,
Sentry.init({
dsn,

// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,

// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: process.env.NODE_ENV === 'development',
ignoreErrors: [/fetch failed/i],
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: process.env.NODE_ENV === 'development',
ignoreErrors: [/fetch failed/i],

beforeSend(event) {
if (!isProd && event.type === undefined) {
return null;
}
return event;
},
});
beforeSend(event) {
if (!isProd && event.type === undefined) {
return null
}
return event
},
})
}
102 changes: 102 additions & 0 deletions src/app/api/comment/comment.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { sendCommentCreateNotifications } from '@/jobs/notifications'
import { sendReplyCreateNotifications } from '@/jobs/notifications/send-reply-create-notifications'
import { InitiatedEntity } from '@/types/common'
import { CreateAttachmentRequestSchema } from '@/types/dto/attachments.dto'
import { CreateComment, UpdateComment } from '@/types/dto/comment.dto'
import { getArrayDifference, getArrayIntersection } from '@/utils/array'
import { getFileNameFromPath } from '@/utils/attachmentUtils'
import { getFilePathFromUrl } from '@/utils/signedUrlReplacer'
import { SupabaseActions } from '@/utils/SupabaseActions'
import { CommentAddedSchema } from '@api/activity-logs/schemas/CommentAddedSchema'
import { ActivityLogger } from '@api/activity-logs/services/activity-logger.service'
import { CommentRepository } from '@api/comment/comment.repository'
Expand All @@ -15,6 +19,8 @@ import { TasksService } from '@api/tasks/tasks.service'
import { ActivityType, Comment, CommentInitiator } from '@prisma/client'
import httpStatus from 'http-status'
import { z } from 'zod'
import { AttachmentsService } from '@api/attachments/attachments.service'
import { getSignedUrl } from '@/utils/signUrl'

export class CommentService extends BaseService {
async create(data: CreateComment) {
Expand Down Expand Up @@ -44,6 +50,23 @@ export class CommentService extends BaseService {
},
})

try {
if (comment.content) {
const newContent = await this.updateCommentIdOfAttachmentsAfterCreation(comment.content, data.taskId, comment.id)
await this.db.comment.update({
where: { id: comment.id },
data: {
content: newContent,
updatedAt: comment.createdAt, //dont updated the updatedAt, because it will show (edited) for recently created comments.
},
})
console.info('CommentService#createComment | Comment content attachments updated for comment ID:', comment.id)
}
} catch (e: unknown) {
await this.db.comment.delete({ where: { id: comment.id } })
console.error('CommentService#createComment | Rolling back comment creation', e)
}

if (!comment.parentId) {
const activityLogger = new ActivityLogger({ taskId: data.taskId, user: this.user })
await activityLogger.log(
Expand Down Expand Up @@ -266,4 +289,83 @@ export class CommentService extends BaseService {
return { ...comment, initiator }
})
}

private async updateCommentIdOfAttachmentsAfterCreation(htmlString: string, task_id: string, commentId: string) {
const imgTagRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/g //expression used to match all img srcs in provided HTML string.
const attachmentTagRegex = /<\s*[a-zA-Z]+\s+[^>]*data-type="attachment"[^>]*src="([^"]+)"[^>]*>/g //expression used to match all attachment srcs in provided HTML string.
let match
const replacements: { originalSrc: string; newUrl: string }[] = []

const newFilePaths: { originalSrc: string; newFilePath: string }[] = []
const copyAttachmentPromises: Promise<void>[] = []
const createAttachmentPayloads = []
const matches: { originalSrc: string; filePath: string; fileName: string }[] = []

while ((match = imgTagRegex.exec(htmlString)) !== null) {
const originalSrc = match[1]
const filePath = getFilePathFromUrl(originalSrc)
const fileName = filePath?.split('/').pop()
if (filePath && fileName) {
matches.push({ originalSrc, filePath, fileName })
}
}

while ((match = attachmentTagRegex.exec(htmlString)) !== null) {
const originalSrc = match[1]
const filePath = getFilePathFromUrl(originalSrc)
const fileName = filePath?.split('/').pop()
if (filePath && fileName) {
matches.push({ originalSrc, filePath, fileName })
}
}

for (const { originalSrc, filePath, fileName } of matches) {
const newFilePath = `${this.user.workspaceId}/${task_id}/comments/${commentId}/${fileName}`
const supabaseActions = new SupabaseActions()

const fileMetaData = await supabaseActions.getMetaData(filePath)
createAttachmentPayloads.push(
CreateAttachmentRequestSchema.parse({
commentId: commentId,
filePath: newFilePath,
fileSize: fileMetaData?.size,
fileType: fileMetaData?.contentType,
fileName: getFileNameFromPath(newFilePath),
}),
)
copyAttachmentPromises.push(supabaseActions.moveAttachment(filePath, newFilePath))
newFilePaths.push({ originalSrc, newFilePath })
}

await Promise.all(copyAttachmentPromises)
const attachmentService = new AttachmentsService(this.user)
if (createAttachmentPayloads.length) {
await attachmentService.createMultipleAttachments(createAttachmentPayloads)
}

const signedUrlPromises = newFilePaths.map(async ({ originalSrc, newFilePath }) => {
const newUrl = await getSignedUrl(newFilePath)
if (newUrl) {
replacements.push({ originalSrc, newUrl })
}
})

await Promise.all(signedUrlPromises)

for (const { originalSrc, newUrl } of replacements) {
htmlString = htmlString.replace(originalSrc, newUrl)
}
// const filePaths = newFilePaths.map(({ newFilePath }) => newFilePath)
// await this.db.scrapMedia.updateMany({
// where: {
// filePath: {
// in: filePaths,
// },
// },
// data: {
// taskId: task_id,
// },
// }) //todo: add support for commentId in scrapMedias.
return htmlString
} //todo: make this resuable since this is highly similar to what we are doing on tasks.
}
23 changes: 21 additions & 2 deletions src/app/api/tasks/tasksShared.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { maxSubTaskDepth } from '@/constants/tasks'
import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users'
import { InternalUsers, Uuid } from '@/types/common'
import { CreateAttachmentRequestSchema } from '@/types/dto/attachments.dto'
import { CreateTaskRequest, CreateTaskRequestSchema, Viewers } from '@/types/dto/tasks.dto'
import { getFileNameFromPath } from '@/utils/attachmentUtils'
import { buildLtree, buildLtreeNodeString } from '@/utils/ltree'
import { getFilePathFromUrl } from '@/utils/signedUrlReplacer'
import { getSignedUrl } from '@/utils/signUrl'
import { SupabaseActions } from '@/utils/SupabaseActions'
import APIError from '@api/core/exceptions/api'
import { BaseService } from '@api/core/services/base.service'
import { UserRole } from '@api/core/types/user'
import { AssigneeType, Prisma, PrismaClient, StateType, Task, TaskTemplate } from '@prisma/client'
import httpStatus from 'http-status'
import z from 'zod'
import APIError from '@api/core/exceptions/api'
import { UserRole } from '@api/core/types/user'
import { AttachmentsService } from '@api/attachments/attachments.service'

//Base class with shared permission logic and methods that both tasks.service.ts and public.service.ts could use
export abstract class TasksSharedService extends BaseService {
Expand Down Expand Up @@ -384,6 +387,7 @@ export abstract class TasksSharedService extends BaseService {

const newFilePaths: { originalSrc: string; newFilePath: string }[] = []
const copyAttachmentPromises: Promise<void>[] = []
const createAttachmentPayloads = []
const matches: { originalSrc: string; filePath: string; fileName: string }[] = []

while ((match = imgTagRegex.exec(htmlString)) !== null) {
Expand All @@ -407,11 +411,26 @@ export abstract class TasksSharedService extends BaseService {
for (const { originalSrc, filePath, fileName } of matches) {
const newFilePath = `${this.user.workspaceId}/${task_id}/${fileName}`
const supabaseActions = new SupabaseActions()

const fileMetaData = await supabaseActions.getMetaData(filePath)
createAttachmentPayloads.push(
CreateAttachmentRequestSchema.parse({
taskId: task_id,
filePath: newFilePath,
fileSize: fileMetaData?.size,
fileType: fileMetaData?.contentType,
fileName: getFileNameFromPath(newFilePath),
}),
)
copyAttachmentPromises.push(supabaseActions.moveAttachment(filePath, newFilePath))
newFilePaths.push({ originalSrc, newFilePath })
}

await Promise.all(copyAttachmentPromises)
if (createAttachmentPayloads.length) {
const attachmentService = new AttachmentsService(this.user)
await attachmentService.createMultipleAttachments(createAttachmentPayloads)
}

const signedUrlPromises = newFilePaths.map(async ({ originalSrc, newFilePath }) => {
const newUrl = await getSignedUrl(newFilePath)
Expand Down
1 change: 1 addition & 0 deletions src/app/api/workers/scrap-medias/scrap-medias.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export class ScrapMediaService {
console.error(error)
throw new APIError(404, 'unable to delete some date from supabase')
}
await db.attachment.deleteMany({ where: { filePath: { in: scrapMediasToDeleteFromBucket } } })
}
if (scrapMediasToDelete.length !== 0) {
const idsToDelete = scrapMediasToDelete.map((id) => `'${id}'`).join(', ')
Expand Down
11 changes: 9 additions & 2 deletions src/app/detail/[task_id]/[user_type]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { HeaderBreadcrumbs } from '@/components/layouts/HeaderBreadcrumbs'
import { SilentError } from '@/components/templates/SilentError'
import { apiUrl } from '@/config'
import { AppMargin, SizeofAppMargin } from '@/hoc/AppMargin'
import { AttachmentProvider } from '@/hoc/PostAttachmentProvider'
import { RealTime } from '@/hoc/RealTime'
import { RealTimeTemplates } from '@/hoc/RealtimeTemplates'
import { WorkspaceResponse } from '@/types/common'
Expand Down Expand Up @@ -213,8 +214,14 @@ export default async function TaskDetailPage(props: {
canCreateSubtasks={params.user_type === UserType.INTERNAL_USER || !!getPreviewMode(tokenPayload)}
/>
)}

<ActivityWrapper task_id={task_id} token={token} tokenPayload={tokenPayload} />
<AttachmentProvider
postAttachment={async (postAttachmentPayload) => {
'use server'
await postAttachment(token, postAttachmentPayload)
}}
>
<ActivityWrapper task_id={task_id} token={token} tokenPayload={tokenPayload} />
</AttachmentProvider>
</TaskDetailsContainer>
</Box>
<Box
Expand Down
Loading
Loading