Skip to content

Commit 3e2f991

Browse files
committed
feat(analytics): add PostHog product analytics
1 parent b0c0ee2 commit 3e2f991

File tree

47 files changed

+987
-5
lines changed

Some content is hidden

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

47 files changed

+987
-5
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use client'
2+
3+
import { useEffect } from 'react'
4+
import { usePostHog } from 'posthog-js/react'
5+
6+
/**
7+
* Fires a `landing_page_viewed` PostHog event on mount.
8+
* Renders nothing — exists only to bridge the server/client boundary
9+
* so the server-rendered landing page can emit analytics.
10+
*/
11+
export function LandingAnalytics() {
12+
const posthog = usePostHog()
13+
14+
useEffect(() => {
15+
posthog?.capture('landing_page_viewed', {})
16+
}, [posthog])
17+
18+
return null
19+
}

apps/sim/app/_shell/providers/session-provider.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ export function SessionProvider({ children }: { children: React.ReactNode }) {
9292
email_verified: data.user.emailVerified,
9393
created_at: data.user.createdAt,
9494
})
95+
96+
const workspaceId = data.session?.activeOrganizationId
97+
if (workspaceId && typeof posthog.group === 'function') {
98+
posthog.group('workspace', workspaceId)
99+
}
95100
} else {
96101
posthog.reset()
97102
}

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: { workspace: resolvedWorkspaceId ?? '' },
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') {

apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
1616
import { generateRequestId } from '@/lib/core/utils/request'
1717
import { deleteDocumentStorageFiles } from '@/lib/knowledge/documents/service'
1818
import { cleanupUnusedTagDefinitions } from '@/lib/knowledge/tags/service'
19+
import { captureServerEvent } from '@/lib/posthog/server'
1920
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
2021
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
2122
import { CONNECTOR_REGISTRY } from '@/connectors/registry'
@@ -351,6 +352,18 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
351352
`[${requestId}] Deleted connector ${connectorId}${deleteDocuments ? ` and ${docCount} documents` : `, kept ${docCount} documents`}`
352353
)
353354

355+
captureServerEvent(
356+
auth.userId,
357+
'knowledge_base_connector_removed',
358+
{
359+
knowledge_base_id: knowledgeBaseId,
360+
workspace_id: writeCheck.knowledgeBase.workspaceId,
361+
connector_type: existingConnector[0].connectorType,
362+
documents_deleted: deleteDocuments ? docCount : 0,
363+
},
364+
{ groups: { workspace: writeCheck.knowledgeBase.workspaceId } }
365+
)
366+
354367
recordAudit({
355368
workspaceId: writeCheck.knowledgeBase.workspaceId,
356369
actorId: auth.userId,

apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
77
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
88
import { generateRequestId } from '@/lib/core/utils/request'
99
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
10+
import { captureServerEvent } from '@/lib/posthog/server'
1011
import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
1112

1213
const logger = createLogger('ConnectorManualSyncAPI')
@@ -55,6 +56,17 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
5556

5657
logger.info(`[${requestId}] Manual sync triggered for connector ${connectorId}`)
5758

59+
captureServerEvent(
60+
auth.userId,
61+
'knowledge_base_connector_synced',
62+
{
63+
knowledge_base_id: knowledgeBaseId,
64+
workspace_id: writeCheck.knowledgeBase.workspaceId,
65+
connector_type: connectorRows[0].connectorType,
66+
},
67+
{ groups: { workspace: writeCheck.knowledgeBase.workspaceId } }
68+
)
69+
5870
recordAudit({
5971
workspaceId: writeCheck.knowledgeBase.workspaceId,
6072
actorId: auth.userId,

0 commit comments

Comments
 (0)