Skip to content

Commit 4d08eed

Browse files
committed
feat(analytics): add PostHog product analytics (#3910)
* feat(analytics): add PostHog product analytics * fix(posthog): fix workspace group via URL params, type errors, and clean up comments * fix(posthog): address PR review - fix pre-tx event, auth_method, paused executions, enterprise cancellation, settings double-fire * chore(posthog): remove unused identifyServerPerson * fix(posthog): isolate processQueuedResumes errors, simplify settings posthog deps * fix(posthog): correctly classify SSO auth_method, fix phantom empty-string workspace groups * fix(posthog): remove usePostHog from memo'd TemplateCard, fix copilot chat phantom workspace group * fix(posthog): eliminate all remaining phantom empty-string workspace groups * fix(posthog): fix cancel route phantom group, remove redundant workspaceId shadow in catch block * fix(posthog): use ids.length for block_removed guard to handle container blocks with descendants * chore(posthog): remove unused removedBlockTypes variable * fix(posthog): remove phantom $set person properties from subscription events * fix(posthog): add passedKnowledgeBaseName to knowledge_base_opened effect deps * fix(posthog): capture currentWorkflowId synchronously before async import to avoid stale closure * fix(posthog): add typed captureEvent wrapper for React components, deduplicate copilot_panel_opened * feat(posthog): add task_created and task_message_sent events, remove copilot_panel_opened * feat(posthog): track task_renamed, task_deleted, task_marked_read, task_marked_unread * feat(analytics): expand posthog event coverage with source tracking and lifecycle events * fix(analytics): flush posthog events on SIGTERM before ECS task termination * fix(analytics): fix posthog in useCallback deps and fire block events for bulk operations
1 parent eca7ee4 commit 4d08eed

File tree

66 files changed

+1421
-24
lines changed

Some content is hidden

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

66 files changed

+1421
-24
lines changed

apps/sim/app/(auth)/signup/signup-form.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
'use client'
22

3-
import { Suspense, useMemo, useRef, useState } from 'react'
3+
import { Suspense, useEffect, useMemo, useRef, useState } from 'react'
44
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
55
import { createLogger } from '@sim/logger'
66
import { Eye, EyeOff, Loader2 } from 'lucide-react'
77
import Link from 'next/link'
88
import { useRouter, useSearchParams } from 'next/navigation'
9+
import { usePostHog } from 'posthog-js/react'
910
import { Input, Label } from '@/components/emcn'
1011
import { client, useSession } from '@/lib/auth/auth-client'
1112
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
1213
import { cn } from '@/lib/core/utils/cn'
1314
import { quickValidateEmail } from '@/lib/messaging/email/validation'
15+
import { captureEvent } from '@/lib/posthog/client'
1416
import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes'
1517
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
1618
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
@@ -81,7 +83,12 @@ function SignupFormContent({
8183
const router = useRouter()
8284
const searchParams = useSearchParams()
8385
const { refetch: refetchSession } = useSession()
86+
const posthog = usePostHog()
8487
const [isLoading, setIsLoading] = useState(false)
88+
89+
useEffect(() => {
90+
captureEvent(posthog, 'signup_page_viewed', {})
91+
}, [posthog])
8592
const [showPassword, setShowPassword] = useState(false)
8693
const [password, setPassword] = useState('')
8794
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use client'
2+
3+
import { useEffect } from 'react'
4+
import { usePostHog } from 'posthog-js/react'
5+
import { captureEvent } from '@/lib/posthog/client'
6+
7+
export function LandingAnalytics() {
8+
const posthog = usePostHog()
9+
10+
useEffect(() => {
11+
captureEvent(posthog, 'landing_page_viewed', {})
12+
}, [posthog])
13+
14+
return null
15+
}

apps/sim/app/(home)/landing.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Templates,
1414
Testimonials,
1515
} from '@/app/(home)/components'
16+
import { LandingAnalytics } from '@/app/(home)/landing-analytics'
1617

1718
/**
1819
* Landing page root component.
@@ -45,6 +46,7 @@ export default async function Landing() {
4546
>
4647
Skip to main content
4748
</a>
49+
<LandingAnalytics />
4850
<StructuredData />
4951
<header>
5052
<Navbar blogPosts={blogPosts} />

apps/sim/app/api/a2a/agents/[agentId]/route.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-c
77
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
88
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
99
import { getRedisClient } from '@/lib/core/config/redis'
10+
import { captureServerEvent } from '@/lib/posthog/server'
1011
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
1112
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1213

@@ -180,6 +181,17 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
180181

181182
logger.info(`Deleted A2A agent: ${agentId}`)
182183

184+
captureServerEvent(
185+
auth.userId,
186+
'a2a_agent_deleted',
187+
{
188+
agent_id: agentId,
189+
workflow_id: existingAgent.workflowId,
190+
workspace_id: existingAgent.workspaceId,
191+
},
192+
{ groups: { workspace: existingAgent.workspaceId } }
193+
)
194+
183195
return NextResponse.json({ success: true })
184196
} catch (error) {
185197
logger.error('Error deleting agent:', error)
@@ -251,6 +263,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
251263
}
252264

253265
logger.info(`Published A2A agent: ${agentId}`)
266+
captureServerEvent(
267+
auth.userId,
268+
'a2a_agent_published',
269+
{
270+
agent_id: agentId,
271+
workflow_id: existingAgent.workflowId,
272+
workspace_id: existingAgent.workspaceId,
273+
},
274+
{ groups: { workspace: existingAgent.workspaceId } }
275+
)
254276
return NextResponse.json({ success: true, isPublished: true })
255277
}
256278

@@ -273,6 +295,16 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
273295
}
274296

275297
logger.info(`Unpublished A2A agent: ${agentId}`)
298+
captureServerEvent(
299+
auth.userId,
300+
'a2a_agent_unpublished',
301+
{
302+
agent_id: agentId,
303+
workflow_id: existingAgent.workflowId,
304+
workspace_id: existingAgent.workspaceId,
305+
},
306+
{ groups: { workspace: existingAgent.workspaceId } }
307+
)
276308
return NextResponse.json({ success: true, isPublished: false })
277309
}
278310

apps/sim/app/api/a2a/agents/route.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
1414
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
1515
import { sanitizeAgentName } from '@/lib/a2a/utils'
1616
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
17+
import { captureServerEvent } from '@/lib/posthog/server'
1718
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
1819
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
1920
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
@@ -201,6 +202,16 @@ export async function POST(request: NextRequest) {
201202

202203
logger.info(`Created A2A agent ${agentId} for workflow ${workflowId}`)
203204

205+
captureServerEvent(
206+
auth.userId,
207+
'a2a_agent_created',
208+
{ agent_id: agentId, workflow_id: workflowId, workspace_id: workspaceId },
209+
{
210+
groups: { workspace: workspaceId },
211+
setOnce: { first_a2a_agent_created_at: new Date().toISOString() },
212+
}
213+
)
214+
204215
return NextResponse.json({ success: true, agent }, { status: 201 })
205216
} catch (error) {
206217
logger.error('Error creating agent:', error)

apps/sim/app/api/billing/switch-plan/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
hasUsableSubscriptionStatus,
1818
} from '@/lib/billing/subscriptions/utils'
1919
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
20+
import { captureServerEvent } from '@/lib/posthog/server'
2021

2122
const logger = createLogger('SwitchPlan')
2223

@@ -173,6 +174,13 @@ export async function POST(request: NextRequest) {
173174
interval: targetInterval,
174175
})
175176

177+
captureServerEvent(
178+
userId,
179+
'subscription_changed',
180+
{ from_plan: sub.plan ?? 'unknown', to_plan: targetPlanName, interval: targetInterval },
181+
{ set: { plan: targetPlanName } }
182+
)
183+
176184
return NextResponse.json({ success: true, plan: targetPlanName, interval: targetInterval })
177185
} catch (error) {
178186
logger.error('Failed to switch subscription', {

apps/sim/app/api/copilot/chat/route.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
createRequestTracker,
2828
createUnauthorizedResponse,
2929
} from '@/lib/copilot/request-helpers'
30+
import { captureServerEvent } from '@/lib/posthog/server'
3031
import {
3132
authorizeWorkflowByWorkspacePermission,
3233
resolveWorkflowIdForUser,
@@ -188,6 +189,22 @@ export async function POST(req: NextRequest) {
188189
.warn('Failed to resolve workspaceId from workflow')
189190
}
190191

192+
captureServerEvent(
193+
authenticatedUserId,
194+
'copilot_chat_sent',
195+
{
196+
workflow_id: workflowId,
197+
workspace_id: resolvedWorkspaceId ?? '',
198+
has_file_attachments: Array.isArray(fileAttachments) && fileAttachments.length > 0,
199+
has_contexts: Array.isArray(contexts) && contexts.length > 0,
200+
mode,
201+
},
202+
{
203+
groups: resolvedWorkspaceId ? { workspace: resolvedWorkspaceId } : undefined,
204+
setOnce: { first_copilot_use_at: new Date().toISOString() },
205+
}
206+
)
207+
191208
const userMessageIdToUse = userMessageId || crypto.randomUUID()
192209
const reqLogger = logger.withMetadata({
193210
requestId: tracker.requestId,

apps/sim/app/api/copilot/feedback/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
createRequestTracker,
1212
createUnauthorizedResponse,
1313
} from '@/lib/copilot/request-helpers'
14+
import { captureServerEvent } from '@/lib/posthog/server'
1415

1516
const logger = createLogger('CopilotFeedbackAPI')
1617

@@ -76,6 +77,12 @@ export async function POST(req: NextRequest) {
7677
duration: tracker.getDuration(),
7778
})
7879

80+
captureServerEvent(authenticatedUserId, 'copilot_feedback_submitted', {
81+
is_positive: isPositiveFeedback,
82+
has_text_feedback: !!feedback,
83+
has_workflow_yaml: !!workflowYaml,
84+
})
85+
7986
return NextResponse.json({
8087
success: true,
8188
feedbackId: feedbackRecord.feedbackId,

apps/sim/app/api/credentials/[id]/route.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
syncPersonalEnvCredentialsForUser,
1212
syncWorkspaceEnvCredentials,
1313
} from '@/lib/credentials/environment'
14+
import { captureServerEvent } from '@/lib/posthog/server'
1415

1516
const logger = createLogger('CredentialByIdAPI')
1617

@@ -236,6 +237,17 @@ export async function DELETE(
236237
envKeys: Object.keys(current),
237238
})
238239

240+
captureServerEvent(
241+
session.user.id,
242+
'credential_deleted',
243+
{
244+
credential_type: 'env_personal',
245+
provider_id: access.credential.envKey,
246+
workspace_id: access.credential.workspaceId,
247+
},
248+
{ groups: { workspace: access.credential.workspaceId } }
249+
)
250+
239251
return NextResponse.json({ success: true }, { status: 200 })
240252
}
241253

@@ -278,10 +290,33 @@ export async function DELETE(
278290
actingUserId: session.user.id,
279291
})
280292

293+
captureServerEvent(
294+
session.user.id,
295+
'credential_deleted',
296+
{
297+
credential_type: 'env_workspace',
298+
provider_id: access.credential.envKey,
299+
workspace_id: access.credential.workspaceId,
300+
},
301+
{ groups: { workspace: access.credential.workspaceId } }
302+
)
303+
281304
return NextResponse.json({ success: true }, { status: 200 })
282305
}
283306

284307
await db.delete(credential).where(eq(credential.id, id))
308+
309+
captureServerEvent(
310+
session.user.id,
311+
'credential_deleted',
312+
{
313+
credential_type: access.credential.type as 'oauth' | 'service_account',
314+
provider_id: access.credential.providerId ?? id,
315+
workspace_id: access.credential.workspaceId,
316+
},
317+
{ groups: { workspace: access.credential.workspaceId } }
318+
)
319+
285320
return NextResponse.json({ success: true }, { status: 200 })
286321
} catch (error) {
287322
logger.error('Failed to delete credential', error)

apps/sim/app/api/credentials/route.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
1010
import { getWorkspaceMemberUserIds } from '@/lib/credentials/environment'
1111
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
1212
import { getServiceConfigByProviderId } from '@/lib/oauth'
13+
import { captureServerEvent } from '@/lib/posthog/server'
1314
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1415
import { isValidEnvVarName } from '@/executor/constants'
1516

@@ -600,6 +601,16 @@ export async function POST(request: NextRequest) {
600601
.where(eq(credential.id, credentialId))
601602
.limit(1)
602603

604+
captureServerEvent(
605+
session.user.id,
606+
'credential_connected',
607+
{ credential_type: type, provider_id: resolvedProviderId ?? type, workspace_id: workspaceId },
608+
{
609+
groups: { workspace: workspaceId },
610+
setOnce: { first_credential_connected_at: new Date().toISOString() },
611+
}
612+
)
613+
603614
return NextResponse.json({ credential: created }, { status: 201 })
604615
} catch (error: any) {
605616
if (error?.code === '23505') {

0 commit comments

Comments
 (0)