diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionRecentInvocations.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionRecentInvocations.tsx
new file mode 100644
index 00000000000..04456c2dc27
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionRecentInvocations.tsx
@@ -0,0 +1,139 @@
+import { useParams } from 'common'
+import { LOGS_TABLES } from 'components/interfaces/Settings/Logs/Logs.constants'
+import useLogsPreview from 'hooks/analytics/useLogsPreview'
+import { Clock, ExternalLink, RefreshCw } from 'lucide-react'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { Button, cn } from 'ui'
+import { Admonition, TimestampInfo } from 'ui-patterns'
+import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+
+import { parseEdgeFunctionEventMessage } from './EdgeFunctionRecentInvocations.utils'
+
+interface EdgeFunctionRecentInvocationsProps {
+ functionId: string
+ functionSlug: string
+}
+
+export const EdgeFunctionRecentInvocations = ({
+ functionId,
+ functionSlug,
+}: EdgeFunctionRecentInvocationsProps) => {
+ const { ref } = useParams()
+ const router = useRouter()
+
+ const { logData, isLoading, isSuccess, refresh } = useLogsPreview({
+ projectRef: ref as string,
+ table: LOGS_TABLES.fn_edge,
+ filterOverride: { function_id: functionId },
+ limit: 10,
+ })
+
+ return (
+
+
+
+
Recent Invocations
+
+ Latest invocation requests for this function
+
+
+
}
+ onClick={() => refresh()}
+ >
+ Refresh
+
+
+
+ {isLoading && !isSuccess ? (
+
+ ) : logData.length === 0 ? (
+
+ ) : (
+
+ {logData.map((log) => {
+ const statusCode = String(log.status_code ?? '')
+ const method = String(log.method ?? '')
+ const executionTime = log.execution_time_ms
+ const is2xx = statusCode.startsWith('2')
+ const is4xx = statusCode.startsWith('4')
+ const is5xx = statusCode.startsWith('5')
+ const logUrl = `/project/${ref}/functions/${functionSlug}/invocations?log=${log.id}`
+
+ return (
+
router.push(logUrl)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ router.push(logUrl)
+ }
+ }}
+ className="group flex items-center font-mono px-3 py-2 gap-3 bg-surface-100 cursor-pointer hover:bg-surface-200 transition-colors"
+ >
+
+
+
+
+ {statusCode ? (
+
+ {statusCode}
+
+ ) : (
+
-
+ )}
+
+
{method || '-'}
+ {executionTime !== undefined && (
+
+
+ {Number(executionTime).toFixed(0)}ms
+
+ )}
+
+ {parseEdgeFunctionEventMessage(
+ String(log.event_message ?? ''),
+ method,
+ statusCode
+ )}
+
+
+
+ )
+ })}
+
+ View all invocations
+
+
+ )}
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionRecentInvocations.utils.test.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionRecentInvocations.utils.test.ts
new file mode 100644
index 00000000000..e5809e2517b
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionRecentInvocations.utils.test.ts
@@ -0,0 +1,87 @@
+import { describe, expect, it } from 'vitest'
+
+import { parseEdgeFunctionEventMessage } from './EdgeFunctionRecentInvocations.utils'
+
+describe('parseEdgeFunctionEventMessage', () => {
+ it('should strip method and status code when they match the structured fields', () => {
+ const message = 'POST | 200 | https://example.supabase.red/functions/v1/hello-world'
+ expect(parseEdgeFunctionEventMessage(message, 'POST', '200')).toBe(
+ 'https://example.supabase.red/functions/v1/hello-world'
+ )
+ })
+
+ it('should strip method and status code for different HTTP methods', () => {
+ const message = 'GET | 404 | https://example.supabase.red/functions/v1/not-found'
+ expect(parseEdgeFunctionEventMessage(message, 'GET', '404')).toBe(
+ 'https://example.supabase.red/functions/v1/not-found'
+ )
+ })
+
+ it('should strip method and status code for 500 errors', () => {
+ const message = 'POST | 500 | https://example.supabase.red/functions/v1/broken'
+ expect(parseEdgeFunctionEventMessage(message, 'POST', '500')).toBe(
+ 'https://example.supabase.red/functions/v1/broken'
+ )
+ })
+
+ it('should preserve the rest of the message when it contains pipe separators', () => {
+ const message = 'POST | 200 | some | complex | message'
+ expect(parseEdgeFunctionEventMessage(message, 'POST', '200')).toBe('some | complex | message')
+ })
+
+ it('should return original message when method does not match', () => {
+ const message = 'POST | 200 | https://example.supabase.red/functions/v1/hello-world'
+ expect(parseEdgeFunctionEventMessage(message, 'GET', '200')).toBe(message)
+ })
+
+ it('should return original message when status code does not match', () => {
+ const message = 'POST | 200 | https://example.supabase.red/functions/v1/hello-world'
+ expect(parseEdgeFunctionEventMessage(message, 'POST', '500')).toBe(message)
+ })
+
+ it('should return original message when method is undefined', () => {
+ const message = 'POST | 200 | https://example.supabase.red/functions/v1/hello-world'
+ expect(parseEdgeFunctionEventMessage(message, undefined, '200')).toBe(message)
+ })
+
+ it('should return original message when status code is undefined', () => {
+ const message = 'POST | 200 | https://example.supabase.red/functions/v1/hello-world'
+ expect(parseEdgeFunctionEventMessage(message, 'POST', undefined)).toBe(message)
+ })
+
+ it('should return original message when both method and status code are undefined', () => {
+ const message = 'POST | 200 | https://example.supabase.red/functions/v1/hello-world'
+ expect(parseEdgeFunctionEventMessage(message, undefined, undefined)).toBe(message)
+ })
+
+ it('should return original message when format has fewer than 3 parts', () => {
+ const message = 'some simple message'
+ expect(parseEdgeFunctionEventMessage(message, 'POST', '200')).toBe(message)
+ })
+
+ it('should return original message when format has only 2 pipe-separated parts', () => {
+ const message = 'POST | 200'
+ expect(parseEdgeFunctionEventMessage(message, 'POST', '200')).toBe(message)
+ })
+
+ it('should return empty string for empty input', () => {
+ expect(parseEdgeFunctionEventMessage('', 'POST', '200')).toBe('')
+ })
+
+ it('should handle whitespace around parts correctly', () => {
+ const message = 'POST | 200 | https://example.supabase.red/functions/v1/hello-world'
+ expect(parseEdgeFunctionEventMessage(message, 'POST', '200')).toBe(
+ 'https://example.supabase.red/functions/v1/hello-world'
+ )
+ })
+
+ it('should return original message when the message does not follow the expected format', () => {
+ const message = 'Error: function crashed unexpectedly'
+ expect(parseEdgeFunctionEventMessage(message, 'POST', '500')).toBe(message)
+ })
+
+ it('should handle messages with extra whitespace in the URL portion', () => {
+ const message = 'DELETE | 204 | '
+ expect(parseEdgeFunctionEventMessage(message, 'DELETE', '204')).toBe('')
+ })
+})
diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionRecentInvocations.utils.ts b/apps/studio/components/interfaces/Functions/EdgeFunctionRecentInvocations.utils.ts
new file mode 100644
index 00000000000..37145dc5787
--- /dev/null
+++ b/apps/studio/components/interfaces/Functions/EdgeFunctionRecentInvocations.utils.ts
@@ -0,0 +1,37 @@
+/**
+ * Parses an edge function invocation event_message to extract the meaningful
+ * content, stripping out the method and status code that are already displayed
+ * as structured fields.
+ *
+ * The event_message typically follows the format:
+ * "{METHOD} | {STATUS_CODE} | {URL_OR_DETAIL}"
+ *
+ * e.g. "POST | 200 | https://example.supabase.red/functions/v1/hello-world"
+ *
+ * When the structured method and status_code fields match what's in the message,
+ * we strip them out to avoid duplication. If parsing fails or the message doesn't
+ * match the expected format, we return the original message as-is.
+ */
+export function parseEdgeFunctionEventMessage(
+ eventMessage: string,
+ method?: string,
+ statusCode?: string
+): string {
+ if (!eventMessage) return eventMessage
+
+ const parts = eventMessage.split(' | ')
+
+ if (parts.length < 3) return eventMessage
+
+ const messageMethod = parts[0].trim()
+ const messageStatus = parts[1].trim()
+
+ const methodMatches = method !== undefined && messageMethod === method
+ const statusMatches = statusCode !== undefined && messageStatus === statusCode
+
+ if (methodMatches && statusMatches) {
+ return parts.slice(2).join(' | ').trim()
+ }
+
+ return eventMessage
+}
diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
index 7c7b05294bd..b2708a94193 100644
--- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
+++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { IS_PLATFORM, useParams } from 'common'
-import { useFlag } from 'common'
+import { IS_PLATFORM, useFlag, useParams } from 'common'
+import { EdgeFunctionRecentInvocations } from 'components/interfaces/Functions/EdgeFunctionRecentInvocations'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
import DefaultLayout from 'components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
@@ -22,9 +22,9 @@ import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import type { ChartIntervals, NextPageWithLayout } from 'types'
import {
+ Alert_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
- Alert_Shadcn_,
Button,
WarningIcon,
} from 'ui'
@@ -138,6 +138,14 @@ const PageLayout: NextPageWithLayout = () => {
+ {IS_PLATFORM && id && (
+
+
+
+ )}
{CHART_INTERVALS.map((item, i) => {