Skip to content

Commit ba55780

Browse files
authored
Implement weekly email schedule job (#2022)
We want to send an email with relevant data for the organizations using Latitude. We'll do the processing of this data on monday 1:00 A.M and it will be sent to be ready on users' inbox on monday morning
1 parent 27e5c2f commit ba55780

File tree

77 files changed

+2738
-292
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+2738
-292
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use server'
2+
3+
import { withAdmin } from '../../procedures'
4+
import { z } from 'zod'
5+
import { queues } from '@latitude-data/core/queues'
6+
7+
export const enqueueWeeklyEmailAction = withAdmin
8+
.inputSchema(
9+
z.object({
10+
workspaceId: z.number(),
11+
emails: z.string().optional(),
12+
}),
13+
)
14+
.action(async ({ parsedInput }) => {
15+
const { workspaceId, emails } = parsedInput
16+
17+
// Parse comma-separated emails if provided
18+
const emailList = emails
19+
? emails
20+
.split(',')
21+
.map((e) => e.trim())
22+
.filter((e) => e.length > 0)
23+
: undefined
24+
25+
const { maintenanceQueue } = await queues()
26+
await maintenanceQueue.add(
27+
'sendWeeklyEmailJob',
28+
{
29+
workspaceId,
30+
emails: emailList,
31+
},
32+
{
33+
jobId: `weekly-email-manual-${workspaceId}-${Date.now()}`,
34+
},
35+
)
36+
37+
return { success: true }
38+
})

apps/web/src/app/(admin)/backoffice/_components/BackofficeTabs/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ export function BackofficeTabs({ children }: { children: ReactNode }) {
6161
value: BackofficeRoutes.integrations,
6262
route: ROUTES.backoffice.integrations.root,
6363
},
64+
{
65+
label: 'Weekly',
66+
value: BackofficeRoutes.weekly,
67+
route: ROUTES.backoffice.weekly.root,
68+
},
6469
]}
6570
selected={selected}
6671
/>

apps/web/src/app/(admin)/backoffice/search/workspace/[id]/_components/WorkspaceDashboard/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ export function WorkspaceDashboard({ workspace }: Props) {
9292
issuesUnlocked={workspace.issuesUnlocked}
9393
/>
9494
<div className='flex gap-2'>
95+
<Link
96+
href={ROUTES.backoffice[BackofficeRoutes.weekly].withWorkspaceId(
97+
workspace.id,
98+
)}
99+
>
100+
<Button fancy variant='outline'>
101+
<Text.H5B noWrap>See Weekly</Text.H5B>
102+
</Button>
103+
</Link>
95104
<ChangePlanButton
96105
workspaceId={workspace.id}
97106
currentPlan={workspace.subscription.plan}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use client'
2+
3+
import { FormEvent, useCallback, useEffect, useState } from 'react'
4+
import { useSearchParams } from 'next/navigation'
5+
import { Button } from '@latitude-data/web-ui/atoms/Button'
6+
import { Input } from '@latitude-data/web-ui/atoms/Input'
7+
import { TextArea } from '@latitude-data/web-ui/atoms/TextArea'
8+
import { Text } from '@latitude-data/web-ui/atoms/Text'
9+
import { useToast } from '@latitude-data/web-ui/atoms/Toast'
10+
import { enqueueWeeklyEmailAction } from '$/actions/admin/weeklyEmail/enqueueWeeklyEmail'
11+
import useLatitudeAction from '$/hooks/useLatitudeAction'
12+
13+
export function WeeklyEmailForm() {
14+
const searchParams = useSearchParams()
15+
const { toast } = useToast()
16+
const [workspaceId, setWorkspaceId] = useState('')
17+
const [emails, setEmails] = useState('')
18+
19+
// Pre-fill workspaceId from query params if present
20+
useEffect(() => {
21+
const workspaceIdParam = searchParams.get('workspaceId')
22+
if (workspaceIdParam) {
23+
setWorkspaceId(workspaceIdParam)
24+
}
25+
}, [searchParams])
26+
27+
const { execute: enqueueEmail, isPending } = useLatitudeAction(
28+
enqueueWeeklyEmailAction,
29+
{
30+
onSuccess: () => {
31+
toast({
32+
title: 'Success',
33+
description: 'Weekly email job has been enqueued successfully',
34+
})
35+
// Clear form
36+
setWorkspaceId('')
37+
setEmails('')
38+
},
39+
onError: (error) => {
40+
toast({
41+
title: 'Error',
42+
description: error.message,
43+
variant: 'destructive',
44+
})
45+
},
46+
},
47+
)
48+
49+
const handleSubmit = useCallback(
50+
(e: FormEvent) => {
51+
e.preventDefault()
52+
53+
const parsedWorkspaceId = parseInt(workspaceId)
54+
if (isNaN(parsedWorkspaceId)) {
55+
toast({
56+
title: 'Invalid Workspace ID',
57+
description: 'Please enter a valid numeric workspace ID',
58+
variant: 'destructive',
59+
})
60+
return
61+
}
62+
63+
enqueueEmail({
64+
workspaceId: parsedWorkspaceId,
65+
emails: emails.trim() || undefined,
66+
})
67+
},
68+
[workspaceId, emails, enqueueEmail, toast],
69+
)
70+
71+
return (
72+
<form onSubmit={handleSubmit} className='flex flex-col gap-y-4 max-w-2xl'>
73+
<div className='flex flex-col gap-y-2'>
74+
<Text.H5 weight='medium'>Workspace ID</Text.H5>
75+
<Input
76+
type='number'
77+
placeholder='Enter workspace ID'
78+
value={workspaceId}
79+
onChange={(e) => setWorkspaceId(e.target.value)}
80+
required
81+
/>
82+
<Text.H6 color='foregroundMuted'>
83+
The ID of the workspace to send the weekly email for
84+
</Text.H6>
85+
</div>
86+
87+
<div className='flex flex-col gap-y-2'>
88+
<Text.H5 weight='medium'>Email Addresses (Optional)</Text.H5>
89+
<TextArea
90+
placeholder='email1@example.com, email2@example.com'
91+
value={emails}
92+
onChange={(e) => setEmails(e.target.value)}
93+
rows={4}
94+
/>
95+
<Text.H6 color='foregroundMuted'>
96+
Comma-separated list of email addresses. If left empty, the email will
97+
be sent to all workspace members who have opted in to receive weekly
98+
emails.
99+
</Text.H6>
100+
</div>
101+
102+
<div className='flex gap-2'>
103+
<Button type='submit' fancy disabled={isPending}>
104+
{isPending ? 'Enqueueing...' : 'Enqueue Weekly Email'}
105+
</Button>
106+
</div>
107+
</form>
108+
)
109+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use client'
2+
3+
import { Text } from '@latitude-data/web-ui/atoms/Text'
4+
import { WeeklyEmailForm } from './_components/WeeklyEmailForm'
5+
6+
export default function AdminWeeklyEmail() {
7+
return (
8+
<div className='container flex flex-col gap-y-8'>
9+
<section className='flex flex-col gap-y-4'>
10+
<Text.H1>Weekly Email</Text.H1>
11+
<Text.H4 color='foregroundMuted'>
12+
Manually trigger weekly email reports for specific workspaces. You can
13+
optionally provide a comma-separated list of email addresses to send
14+
the report to instead of the workspace members.
15+
</Text.H4>
16+
<WeeklyEmailForm />
17+
</section>
18+
</div>
19+
)
20+
}

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/ProjectSection/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useMemo } from 'react'
4-
import { formatCount } from '$/lib/formatCount'
4+
import { formatCount } from '@latitude-data/constants/formatCount'
55
import { ROUTES } from '$/services/routes'
66

77
import { RunSourceGroup } from '@latitude-data/constants'

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/(withTabs)/evaluations/[evaluationUuid]/_components/charts/DailyOverview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { formatCostInMillicents } from '$/app/_lib/formatUtils'
22
import { useCurrentEvaluationV2 } from '$/app/providers/EvaluationV2Provider'
33
import { EVALUATION_SPECIFICATIONS } from '$/components/evaluations'
4-
import { formatCount } from '$/lib/formatCount'
4+
import { formatCount } from '@latitude-data/constants/formatCount'
55
import { ChartBlankSlate } from '@latitude-data/web-ui/atoms/ChartBlankSlate'
66
import { Text } from '@latitude-data/web-ui/atoms/Text'
77
import { AreaChart, ChartWrapper } from '@latitude-data/web-ui/molecules/Charts'

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/(withTabs)/evaluations/[evaluationUuid]/_components/charts/VersionOverview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { formatCostInMillicents } from '$/app/_lib/formatUtils'
22
import { useCurrentEvaluationV2 } from '$/app/providers/EvaluationV2Provider'
33
import { EVALUATION_SPECIFICATIONS } from '$/components/evaluations'
4-
import { formatCount } from '$/lib/formatCount'
4+
import { formatCount } from '@latitude-data/constants/formatCount'
55
import { Badge } from '@latitude-data/web-ui/atoms/Badge'
66
import { ChartBlankSlate } from '@latitude-data/web-ui/atoms/ChartBlankSlate'
77
import { Text } from '@latitude-data/web-ui/atoms/Text'

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/(withTabs)/experiments/_components/ExperimentsTable/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { useCurrentProject } from '$/app/providers/ProjectProvider'
1818
import { DurationCell } from './DurationCell'
1919
import { ScoreCell } from './ScoreCell'
2020
import { cn } from '@latitude-data/web-ui/utils'
21-
import { formatCount } from '$/lib/formatCount'
21+
import { formatCount } from '@latitude-data/constants/formatCount'
2222
import { useSearchParams } from 'next/navigation'
2323
import { LinkableTablePaginationFooter } from '$/components/TablePaginationFooter'
2424
import { DocumentRoutes, ROUTES } from '$/services/routes'

apps/web/src/app/api/spans/limited/route.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ export const GET = errorHandler(
5656
: null
5757

5858
let spansResult
59+
const spansRepository = new SpansRepository(workspace.id)
60+
5961
if (filters.traceId) {
60-
// If traceId is present in filters, fetch spans for that specific trace
61-
const spansRepository = new SpansRepository(workspace.id)
6262
const spans = await spansRepository
6363
.list({ traceId: filters.traceId })
6464
.then((r) =>
@@ -70,13 +70,9 @@ export const GET = errorHandler(
7070
next: null,
7171
}
7272
} else {
73-
// Otherwise, fetch spans directly from the repository
74-
const spansRepository = new SpansRepository(workspace.id)
75-
7673
let result: { items: any[]; next: any } = { items: [], next: null }
7774

7875
if (documentUuid) {
79-
// Document queries require commitUuid
8076
if (!commitUuid) {
8177
return NextResponse.json(
8278
{ error: 'commitUuid is required when documentUuid is provided' },

0 commit comments

Comments
 (0)