mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-19 08:28:10 +02:00
feat: add posthog events (#231)
* feat: add posthog events * fix: workflow_duplicated event * chore: add events to enum
This commit is contained in:
parent
bb5f56bfb7
commit
3f19a16e7f
24 changed files with 450 additions and 93 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { Loader2, Mic, Pause, Play, Square, Trash2Icon, Upload, X } from "lucide-react";
|
||||
import posthog from "posthog-js";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
|
|
@ -28,6 +29,7 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { LANGUAGE_DISPLAY_NAMES } from "@/constants/languages";
|
||||
import { PostHogEvent } from "@/constants/posthog-events";
|
||||
import { useUserConfig } from "@/context/UserConfigContext";
|
||||
import { useAudioPlayback } from "@/hooks/useAudioPlayback";
|
||||
|
||||
|
|
@ -357,6 +359,10 @@ export const RecordingsDialog = ({
|
|||
const handlePlay = async (rec: RecordingResponseSchema) => {
|
||||
try {
|
||||
await togglePlayback(rec.recording_id, rec.storage_key, rec.storage_backend);
|
||||
posthog.capture(PostHogEvent.RECORDING_PLAYED, {
|
||||
recording_id: rec.recording_id,
|
||||
source: 'recordings_dialog',
|
||||
});
|
||||
} catch {
|
||||
setError("Failed to play recording");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { ReactFlowInstance } from "@xyflow/react";
|
||||
import { AlertCircle, ArrowLeft, ChevronDown, Copy, Download, Eye, History, LoaderCircle, MoreVertical, Phone, Rocket } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import posthog from "posthog-js";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ import {
|
|||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { PostHogEvent } from "@/constants/posthog-events";
|
||||
import { WORKFLOW_RUN_MODES } from "@/constants/workflowRunModes";
|
||||
|
||||
interface WorkflowEditorHeaderProps {
|
||||
|
|
@ -259,7 +261,13 @@ export const WorkflowEditorHeader = ({
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="bg-[#1a1a1a] border-[#3a3a3a]">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onRun(WORKFLOW_RUN_MODES.SMALL_WEBRTC)}
|
||||
onClick={() => {
|
||||
posthog.capture(PostHogEvent.WEB_CALL_INITIATED, {
|
||||
workflow_id: workflowId,
|
||||
workflow_name: workflowName,
|
||||
});
|
||||
onRun(WORKFLOW_RUN_MODES.SMALL_WEBRTC);
|
||||
}}
|
||||
className="text-white hover:bg-[#2a2a2a] cursor-pointer"
|
||||
>
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from "@xyflow/react";
|
||||
import { EdgeChange, NodeChange } from "@xyflow/system";
|
||||
import { useRouter } from "next/navigation";
|
||||
import posthog from "posthog-js";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { useWorkflowStore } from "@/app/workflow/[workflowId]/stores/workflowStore";
|
||||
|
|
@ -18,6 +19,7 @@ import {
|
|||
} from "@/client";
|
||||
import { WorkflowError } from "@/client/types.gen";
|
||||
import { FlowEdge, FlowNode, NodeType } from "@/components/flow/types";
|
||||
import { PostHogEvent } from "@/constants/posthog-events";
|
||||
import logger from '@/lib/logger';
|
||||
import { getNextNodeId, getRandomId } from "@/lib/utils";
|
||||
import { DEFAULT_WORKFLOW_CONFIGURATIONS, WorkflowConfigurations } from "@/types/workflow-configurations";
|
||||
|
|
@ -290,8 +292,12 @@ export const useWorkflowState = ({
|
|||
|
||||
// Use addNodes from ReactFlow instance
|
||||
rfInstance.current.addNodes([newNode]);
|
||||
posthog.capture(PostHogEvent.WORKFLOW_NODE_ADDED, {
|
||||
node_type: nodeType,
|
||||
workflow_id: workflowId,
|
||||
});
|
||||
setIsAddNodePanelOpen(false);
|
||||
}, [nodes, setIsAddNodePanelOpen]);
|
||||
}, [nodes, setIsAddNodePanelOpen, workflowId]);
|
||||
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setWorkflowName(e.target.value);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import posthog from 'posthog-js';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import RenderWorkflow from '@/app/workflow/[workflowId]/RenderWorkflow';
|
||||
|
|
@ -8,6 +9,7 @@ import { getWorkflowApiV1WorkflowFetchWorkflowIdGet } from '@/client/sdk.gen';
|
|||
import type { WorkflowResponse } from '@/client/types.gen';
|
||||
import { FlowEdge, FlowNode } from '@/components/flow/types';
|
||||
import SpinLoader from '@/components/SpinLoader';
|
||||
import { PostHogEvent } from '@/constants/posthog-events';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import logger from '@/lib/logger';
|
||||
import { DEFAULT_WORKFLOW_CONFIGURATIONS, WorkflowConfigurations } from '@/types/workflow-configurations';
|
||||
|
|
@ -39,6 +41,10 @@ export default function WorkflowDetailPage() {
|
|||
});
|
||||
const workflow = response.data;
|
||||
setWorkflow(workflow);
|
||||
posthog.capture(PostHogEvent.WORKFLOW_EDITOR_OPENED, {
|
||||
workflow_id: workflow?.id,
|
||||
workflow_name: workflow?.name,
|
||||
});
|
||||
} catch (err) {
|
||||
setError('Failed to fetch workflow');
|
||||
logger.error(`Error fetching workflow: ${err}`);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { Check, Copy, ExternalLink, FileText, LoaderCircle, Phone, Video } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import posthog from 'posthog-js';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import BrowserCall from '@/app/workflow/[workflowId]/run/[runId]/BrowserCall';
|
||||
|
|
@ -17,6 +18,7 @@ import { OnboardingTooltip } from '@/components/onboarding/OnboardingTooltip';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { PostHogEvent } from '@/constants/posthog-events';
|
||||
import { WORKFLOW_RUN_MODES } from '@/constants/workflowRunModes';
|
||||
import { useOnboarding } from '@/context/OnboardingContext';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
|
|
@ -114,7 +116,7 @@ export default function WorkflowRunPage() {
|
|||
},
|
||||
});
|
||||
setIsLoading(false);
|
||||
setWorkflowRun({
|
||||
const runData = {
|
||||
is_completed: response.data?.is_completed ?? false,
|
||||
transcript_url: response.data?.transcript_url ?? null,
|
||||
recording_url: response.data?.recording_url ?? null,
|
||||
|
|
@ -122,6 +124,14 @@ export default function WorkflowRunPage() {
|
|||
gathered_context: response.data?.gathered_context as Record<string, string> | null ?? null,
|
||||
logs: response.data?.logs as WorkflowRunLogs | null ?? null,
|
||||
annotations: response.data?.annotations as Record<string, unknown> | null ?? null,
|
||||
};
|
||||
setWorkflowRun(runData);
|
||||
posthog.capture(PostHogEvent.WORKFLOW_RUN_DETAILS_VIEWED, {
|
||||
workflow_id: Number(workflowId),
|
||||
run_id: Number(runId),
|
||||
is_completed: runData.is_completed,
|
||||
has_recording: !!runData.recording_url,
|
||||
has_transcript: !!runData.transcript_url,
|
||||
});
|
||||
};
|
||||
fetchWorkflowRun();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { Headphones, Loader2 } from 'lucide-react';
|
||||
import posthog from 'posthog-js';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
|
@ -12,6 +13,7 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { PostHogEvent } from '@/constants/posthog-events';
|
||||
import { downloadFile, getSignedUrl } from '@/lib/files';
|
||||
|
||||
export function MediaPreviewDialog() {
|
||||
|
|
@ -48,6 +50,11 @@ export function MediaPreviewDialog() {
|
|||
const response = await fetch(transcriptResult);
|
||||
const text = await response.text();
|
||||
setTranscriptContent(text);
|
||||
posthog.capture(PostHogEvent.TRANSCRIPT_VIEWED, {
|
||||
run_id: runId,
|
||||
source: 'media_preview_dialog',
|
||||
transcript_length: text.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching transcript:', error);
|
||||
}
|
||||
|
|
@ -78,7 +85,16 @@ export function MediaPreviewDialog() {
|
|||
)}
|
||||
|
||||
{!mediaLoading && audioSignedUrl && (
|
||||
<audio src={audioSignedUrl} controls autoPlay className="w-full mt-4" />
|
||||
<audio
|
||||
src={audioSignedUrl}
|
||||
controls
|
||||
autoPlay
|
||||
className="w-full mt-4"
|
||||
onPlay={() => posthog.capture(PostHogEvent.RECORDING_PLAYED, {
|
||||
run_id: selectedRunId,
|
||||
source: 'media_preview_dialog',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!mediaLoading && transcriptContent && (
|
||||
|
|
|
|||
|
|
@ -8,39 +8,67 @@ import { useAuth } from '@/lib/auth';
|
|||
/**
|
||||
* PostHogIdentify
|
||||
* ---------------
|
||||
* A tiny client-side component that calls `posthog.identify` once the
|
||||
* authenticated user object is available. It also resets PostHog when the
|
||||
* user logs out or switches accounts.
|
||||
* Calls `posthog.identify` once the authenticated user object is available,
|
||||
* using the Stack Auth user ID as distinct_id with email and name as properties.
|
||||
*
|
||||
* This component is intended to be rendered high in the React tree (e.g. in
|
||||
* `app/layout.tsx`) so that PostHog always knows which user is active for the
|
||||
* current browser session.
|
||||
* Rendered in the root layout (`app/layout.tsx`) so it fires on every session
|
||||
* for every logged-in user.
|
||||
*/
|
||||
export default function PostHogIdentify() {
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
// Only run if PostHog is enabled
|
||||
if (process.env.NEXT_PUBLIC_ENABLE_POSTHOG !== 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
try {
|
||||
// Identify the user in PostHog with their unique id and useful traits
|
||||
posthog.identify(String(user.id ?? ''));
|
||||
} catch (err) {
|
||||
// Silently ignore identification errors so they don't break the app
|
||||
const identify = () => {
|
||||
try {
|
||||
// Stack Auth users expose primaryEmail/displayName,
|
||||
// local users expose email/name — handle both.
|
||||
const email =
|
||||
'primaryEmail' in user ? user.primaryEmail :
|
||||
'email' in user ? user.email :
|
||||
undefined;
|
||||
const name =
|
||||
'displayName' in user ? user.displayName :
|
||||
'name' in user ? user.name :
|
||||
undefined;
|
||||
|
||||
console.warn('Failed to identify user in PostHog', err);
|
||||
// Use provider_id as distinct_id to match backend PostHog events.
|
||||
// Stack Auth users: user.id is already the provider_id.
|
||||
// OSS users: provider_id is returned from the auth API.
|
||||
const distinctId =
|
||||
'provider_id' in user && user.provider_id
|
||||
? String(user.provider_id)
|
||||
: String(user.id);
|
||||
|
||||
posthog.identify(distinctId, {
|
||||
...(email && { email }),
|
||||
...(name && { name }),
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('Failed to identify user in PostHog', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (posthog.__loaded) {
|
||||
identify();
|
||||
} else {
|
||||
// PostHog initializes async — retry until ready
|
||||
let attempts = 0;
|
||||
const interval = setInterval(() => {
|
||||
attempts++;
|
||||
if (posthog.__loaded) {
|
||||
identify();
|
||||
clearInterval(interval);
|
||||
} else if (attempts >= 20) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 200);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
} else {
|
||||
// If the user logs out, clear the PostHog identity so future anonymous
|
||||
// interactions aren't associated with the previous account.
|
||||
posthog.reset();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// This component does not render anything
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
11
ui/src/constants/posthog-events.ts
Normal file
11
ui/src/constants/posthog-events.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* PostHog event names — frontend events only.
|
||||
*/
|
||||
export const PostHogEvent = {
|
||||
WORKFLOW_EDITOR_OPENED: "workflow_editor_opened",
|
||||
WORKFLOW_NODE_ADDED: "workflow_node_added",
|
||||
WORKFLOW_RUN_DETAILS_VIEWED: "workflow_run_details_viewed",
|
||||
RECORDING_PLAYED: "recording_played",
|
||||
TRANSCRIPT_VIEWED: "transcript_viewed",
|
||||
WEB_CALL_INITIATED: "web_call_initiated",
|
||||
} as const;
|
||||
|
|
@ -13,6 +13,7 @@ export interface LocalUser extends BaseUser {
|
|||
provider: 'local';
|
||||
organizationId?: string;
|
||||
displayName?: string;
|
||||
provider_id?: string;
|
||||
}
|
||||
|
||||
// Union type for all user types
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue