Skip to content

Commit d1f0d21

Browse files
authored
improvement(logs): surface integration triggers in logs instead of catchall 'webhook' trigger type (#2102)
* improvement(logs): surface integration triggers in logs instead of catchall * optimized calculation, added new triggers to search * cleanup * fix console log
1 parent 33ac828 commit d1f0d21

File tree

11 files changed

+424
-131
lines changed

11 files changed

+424
-131
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { db } from '@sim/db'
2+
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
3+
import { and, eq, isNotNull, sql } from 'drizzle-orm'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { z } from 'zod'
6+
import { getSession } from '@/lib/auth'
7+
import { createLogger } from '@/lib/logs/console/logger'
8+
import { generateRequestId } from '@/lib/utils'
9+
10+
const logger = createLogger('TriggersAPI')
11+
12+
export const revalidate = 0
13+
14+
const QueryParamsSchema = z.object({
15+
workspaceId: z.string(),
16+
})
17+
18+
/**
19+
* GET /api/logs/triggers
20+
*
21+
* Returns unique trigger types from workflow execution logs
22+
* Only includes integration triggers (excludes core types: api, manual, webhook, chat, schedule)
23+
*/
24+
export async function GET(request: NextRequest) {
25+
const requestId = generateRequestId()
26+
27+
try {
28+
const session = await getSession()
29+
if (!session?.user?.id) {
30+
logger.warn(`[${requestId}] Unauthorized triggers access attempt`)
31+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
32+
}
33+
34+
const userId = session.user.id
35+
36+
try {
37+
const { searchParams } = new URL(request.url)
38+
const params = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
39+
40+
const triggers = await db
41+
.selectDistinct({
42+
trigger: workflowExecutionLogs.trigger,
43+
})
44+
.from(workflowExecutionLogs)
45+
.innerJoin(
46+
workflow,
47+
and(
48+
eq(workflowExecutionLogs.workflowId, workflow.id),
49+
eq(workflow.workspaceId, params.workspaceId)
50+
)
51+
)
52+
.innerJoin(
53+
permissions,
54+
and(
55+
eq(permissions.entityType, 'workspace'),
56+
eq(permissions.entityId, workflow.workspaceId),
57+
eq(permissions.userId, userId)
58+
)
59+
)
60+
.where(
61+
and(
62+
isNotNull(workflowExecutionLogs.trigger),
63+
sql`${workflowExecutionLogs.trigger} NOT IN ('api', 'manual', 'webhook', 'chat', 'schedule')`
64+
)
65+
)
66+
67+
const triggerValues = triggers
68+
.map((row) => row.trigger)
69+
.filter((t): t is string => Boolean(t))
70+
.sort()
71+
72+
return NextResponse.json({
73+
triggers: triggerValues,
74+
count: triggerValues.length,
75+
})
76+
} catch (err) {
77+
if (err instanceof z.ZodError) {
78+
logger.error(`[${requestId}] Invalid query parameters`, { error: err })
79+
return NextResponse.json(
80+
{ error: 'Invalid query parameters', details: err.errors },
81+
{ status: 400 }
82+
)
83+
}
84+
85+
throw err
86+
}
87+
} catch (err) {
88+
logger.error(`[${requestId}] Failed to fetch triggers`, { error: err })
89+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
90+
}
91+
}

apps/sim/app/workspace/[workspaceId]/logs/components/filters/components/trigger.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
DropdownMenuContent,
1515
DropdownMenuTrigger,
1616
} from '@/components/ui/dropdown-menu'
17+
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
1718
import {
1819
commandListClass,
1920
dropdownContentClass,
@@ -26,15 +27,9 @@ import type { TriggerType } from '@/stores/logs/filters/types'
2627
export default function Trigger() {
2728
const { triggers, toggleTrigger, setTriggers } = useFilterStore()
2829
const [search, setSearch] = useState('')
29-
const triggerOptions: { value: TriggerType; label: string; color?: string }[] = [
30-
{ value: 'manual', label: 'Manual', color: 'bg-gray-500' },
31-
{ value: 'api', label: 'API', color: 'bg-blue-500' },
32-
{ value: 'webhook', label: 'Webhook', color: 'bg-orange-500' },
33-
{ value: 'schedule', label: 'Schedule', color: 'bg-green-500' },
34-
{ value: 'chat', label: 'Chat', color: 'bg-purple-500' },
35-
]
3630

37-
// Get display text for the dropdown button
31+
const triggerOptions = useMemo(() => getTriggerOptions(), [])
32+
3833
const getSelectedTriggersText = () => {
3934
if (triggers.length === 0) return 'All triggers'
4035
if (triggers.length === 1) {
@@ -44,12 +39,10 @@ export default function Trigger() {
4439
return `${triggers.length} triggers selected`
4540
}
4641

47-
// Check if a trigger is selected
4842
const isTriggerSelected = (trigger: TriggerType) => {
4943
return triggers.includes(trigger)
5044
}
5145

52-
// Clear all selections
5346
const clearSelections = () => {
5447
setTriggers([])
5548
}
@@ -98,7 +91,10 @@ export default function Trigger() {
9891
>
9992
<div className='flex items-center'>
10093
{triggerItem.color && (
101-
<div className={`mr-2 h-2 w-2 rounded-full ${triggerItem.color}`} />
94+
<div
95+
className='mr-2 h-2 w-2 rounded-full'
96+
style={{ backgroundColor: triggerItem.color }}
97+
/>
10298
)}
10399
{triggerItem.label}
104100
</div>

apps/sim/app/workspace/[workspaceId]/logs/components/search/search.tsx

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@
22

33
import { useEffect, useMemo, useState } from 'react'
44
import { Search, X } from 'lucide-react'
5+
import { useParams } from 'next/navigation'
56
import { Button, Popover, PopoverAnchor, PopoverContent } from '@/components/emcn'
7+
import { createLogger } from '@/lib/logs/console/logger'
8+
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
69
import { type ParsedFilter, parseQuery } from '@/lib/logs/query-parser'
710
import {
811
type FolderData,
912
SearchSuggestions,
13+
type TriggerData,
1014
type WorkflowData,
1115
} from '@/lib/logs/search-suggestions'
1216
import { cn } from '@/lib/utils'
1317
import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-search-state'
1418
import { useFolderStore } from '@/stores/folders/store'
1519
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1620

21+
const logger = createLogger('AutocompleteSearch')
22+
1723
interface AutocompleteSearchProps {
1824
value: string
1925
onChange: (value: string) => void
@@ -29,8 +35,11 @@ export function AutocompleteSearch({
2935
className,
3036
onOpenChange,
3137
}: AutocompleteSearchProps) {
38+
const params = useParams()
39+
const workspaceId = params.workspaceId as string
3240
const workflows = useWorkflowRegistry((state) => state.workflows)
3341
const folders = useFolderStore((state) => state.folders)
42+
const [triggersData, setTriggersData] = useState<TriggerData[]>([])
3443

3544
const workflowsData = useMemo<WorkflowData[]>(() => {
3645
return Object.values(workflows).map((w) => ({
@@ -47,9 +56,36 @@ export function AutocompleteSearch({
4756
}))
4857
}, [folders])
4958

59+
useEffect(() => {
60+
if (!workspaceId) return
61+
62+
const fetchTriggers = async () => {
63+
try {
64+
const response = await fetch(`/api/logs/triggers?workspaceId=${workspaceId}`)
65+
if (!response.ok) return
66+
67+
const data = await response.json()
68+
const triggers: TriggerData[] = data.triggers.map((trigger: string) => {
69+
const metadata = getIntegrationMetadata(trigger)
70+
return {
71+
value: trigger,
72+
label: metadata.label,
73+
color: metadata.color,
74+
}
75+
})
76+
77+
setTriggersData(triggers)
78+
} catch (error) {
79+
logger.error('Failed to fetch triggers:', error)
80+
}
81+
}
82+
83+
fetchTriggers()
84+
}, [workspaceId])
85+
5086
const suggestionEngine = useMemo(() => {
51-
return new SearchSuggestions(workflowsData, foldersData)
52-
}, [workflowsData, foldersData])
87+
return new SearchSuggestions(workflowsData, foldersData, triggersData)
88+
}, [workflowsData, foldersData, triggersData])
5389

5490
const handleFiltersChange = (filters: ParsedFilter[], textSearch: string) => {
5591
const filterStrings = filters.map(
@@ -84,7 +120,6 @@ export function AutocompleteSearch({
84120
getSuggestions: (input) => suggestionEngine.getSuggestions(input),
85121
})
86122

87-
// Initialize from external value (URL params) - only on mount
88123
useEffect(() => {
89124
if (value) {
90125
const parsed = parseQuery(value)
@@ -269,8 +304,16 @@ export function AutocompleteSearch({
269304
}}
270305
>
271306
<div className='flex items-center justify-between gap-3'>
272-
<div className='min-w-0 flex-1 truncate text-[13px]'>
273-
{suggestion.label}
307+
<div className='flex min-w-0 flex-1 items-center gap-2'>
308+
{suggestion.category === 'trigger' && suggestion.color && (
309+
<div
310+
className='h-2 w-2 flex-shrink-0 rounded-full'
311+
style={{ backgroundColor: suggestion.color }}
312+
/>
313+
)}
314+
<div className='min-w-0 flex-1 truncate text-[13px]'>
315+
{suggestion.label}
316+
</div>
274317
</div>
275318
{suggestion.value !== suggestion.label && (
276319
<div className='flex-shrink-0 font-mono text-[11px] text-[var(--text-muted)]'>
@@ -313,7 +356,15 @@ export function AutocompleteSearch({
313356
}}
314357
>
315358
<div className='flex items-center justify-between gap-3'>
316-
<div className='min-w-0 flex-1 text-[13px]'>{suggestion.label}</div>
359+
<div className='flex min-w-0 flex-1 items-center gap-2'>
360+
{suggestion.category === 'trigger' && suggestion.color && (
361+
<div
362+
className='h-2 w-2 flex-shrink-0 rounded-full'
363+
style={{ backgroundColor: suggestion.color }}
364+
/>
365+
)}
366+
<div className='min-w-0 flex-1 text-[13px]'>{suggestion.label}</div>
367+
</div>
317368
{suggestion.description && (
318369
<div className='flex-shrink-0 text-[11px] text-[var(--text-muted)]'>
319370
{suggestion.value}

apps/sim/app/workspace/[workspaceId]/logs/components/sidebar/sidebar.tsx

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Button, Tooltip } from '@/components/emcn'
1010
import { CopyButton } from '@/components/ui/copy-button'
1111
import { ScrollArea } from '@/components/ui/scroll-area'
1212
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
13+
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
1314
import { FrozenCanvasModal } from '@/app/workspace/[workspaceId]/logs/components/frozen-canvas/frozen-canvas-modal'
1415
import { FileDownload } from '@/app/workspace/[workspaceId]/logs/components/sidebar/components/file-download'
1516
import LogMarkdownRenderer from '@/app/workspace/[workspaceId]/logs/components/sidebar/components/markdown-renderer'
@@ -52,49 +53,6 @@ const tryPrettifyJson = (content: string): { isJson: boolean; formatted: string
5253
}
5354
}
5455

55-
/**
56-
* Formats JSON content for display, handling multiple JSON objects separated by '--'
57-
*/
58-
const formatJsonContent = (content: string, blockInput?: Record<string, any>): React.ReactNode => {
59-
const blockPattern = /^(Block .+?\(.+?\):)\s*/
60-
const match = content.match(blockPattern)
61-
62-
if (match) {
63-
const systemComment = match[1]
64-
const actualContent = content.substring(match[0].length).trim()
65-
const { isJson, formatted } = tryPrettifyJson(actualContent)
66-
67-
return (
68-
<BlockContentDisplay
69-
systemComment={systemComment}
70-
formatted={formatted}
71-
isJson={isJson}
72-
blockInput={blockInput}
73-
/>
74-
)
75-
}
76-
77-
const { isJson, formatted } = tryPrettifyJson(content)
78-
79-
return (
80-
<div className='group relative w-full rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F] p-3'>
81-
<CopyButton text={formatted} className='z-10 h-7 w-7' />
82-
{isJson ? (
83-
<div className='code-editor-theme'>
84-
<pre
85-
className='max-h-[500px] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-all font-mono text-[#eeeeee] text-[11px] leading-[16px]'
86-
dangerouslySetInnerHTML={{
87-
__html: highlight(formatted, languages.json, 'json'),
88-
}}
89-
/>
90-
</div>
91-
) : (
92-
<LogMarkdownRenderer content={formatted} />
93-
)}
94-
</div>
95-
)
96-
}
97-
9856
const BlockContentDisplay = ({
9957
systemComment,
10058
formatted,
@@ -212,11 +170,9 @@ export function Sidebar({
212170
const [isFrozenCanvasOpen, setIsFrozenCanvasOpen] = useState(false)
213171
const scrollAreaRef = useRef<HTMLDivElement>(null)
214172

215-
// Update currentLogId when log changes
216173
useEffect(() => {
217174
if (log?.id) {
218175
setCurrentLogId(log.id)
219-
// Reset trace expanded state when log changes
220176
setIsTraceExpanded(false)
221177
}
222178
}, [log?.id])
@@ -260,6 +216,11 @@ export function Sidebar({
260216
return isWorkflowExecutionLog && hasCostInfo
261217
}, [isWorkflowExecutionLog, hasCostInfo])
262218

219+
const triggerMetadata = useMemo(
220+
() => (log?.trigger ? getIntegrationMetadata(log.trigger) : null),
221+
[log?.trigger]
222+
)
223+
263224
const handleTraceSpanToggle = (expanded: boolean) => {
264225
setIsTraceExpanded(expanded)
265226

@@ -465,14 +426,14 @@ export function Sidebar({
465426
</div>
466427

467428
{/* Trigger */}
468-
{log.trigger && (
429+
{log.trigger && triggerMetadata && (
469430
<div>
470431
<h3 className='mb-[4px] font-medium text-[12px] text-[var(--text-tertiary)] dark:text-[var(--text-tertiary)]'>
471432
Trigger
472433
</h3>
473-
<div className='group relative text-[13px] capitalize'>
434+
<div className='group relative text-[13px]'>
474435
<CopyButton text={log.trigger} />
475-
{log.trigger}
436+
{triggerMetadata.label}
476437
</div>
477438
</div>
478439
)}

0 commit comments

Comments
 (0)