Skip to content

Commit a51b615

Browse files
committed
feat(analytics): wire growth event tracking on PostHog base
Add the analytics events the activation funnel needs from the desktop client side, on top of the PR #778 PostHog migration: - workspace_growth_rewards_click — fired when the sidebar "share nexu / earn extra credits" growth banner is clicked. - workspace_click_usage_detail — fired when the credits balance popup "view usage detail" button is clicked. - user_message_sent.state — controller analytics service now tags each user_message_sent with state="Success" or state="false". Failure is detected by openclaw:prompt-error transcript entries arriving before the assistant response; success is the default and gets confirmed when the assistant message lands. - AnalyticsAuthSource type now also accepts "home" so the home page can be the source of an auth click. The desktop side of the signup_success / login_success plumbing (source pass-through to cloud) is still pending — that side has to be done together with the cloud auth-init schema and is intentionally left out of this commit. The user_message_sent.credit_charged property the funnel spec also mentions cannot be implemented client-side: there is no per-message credit consumption pipeline yet. cloud's credit_usages table is empty of llm-call entries, link writes link.usage_events (USD cost) but nothing transforms USD into credit and writes credit_usages, and link does not record channel either. That work belongs to the cloud team under the existing credit consumption track and is documented in specs/current/credit.md.
1 parent be5cf2b commit a51b615

3 files changed

Lines changed: 44 additions & 1 deletion

File tree

apps/controller/src/services/analytics-service.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,15 @@ type TranscriptEntry = {
3939
};
4040
};
4141

42+
type AnalyticsMessageState = "Success" | "false";
43+
4244
type UserMessageCandidate = {
4345
id: string;
4446
timestampMs: number;
4547
createdAt: string | null;
4648
providerName: string | null;
4749
channel: AnalyticsChannel;
50+
state: AnalyticsMessageState;
4851
};
4952

5053
type SkillUseCandidate = {
@@ -217,6 +220,7 @@ export class AnalyticsService {
217220
{
218221
channel: userMessage.channel,
219222
model_provider: userMessage.providerName,
223+
state: userMessage.state,
220224
},
221225
userMessage.timestampMs,
222226
);
@@ -447,6 +451,38 @@ export class AnalyticsService {
447451
continue;
448452
}
449453

454+
// Resolve any pending user messages as failed when openclaw reports a
455+
// prompt error. Each error entry's parentId points back to the user
456+
// message that triggered it; the cheapest correct interpretation is
457+
// "any user message that hasn't yet been answered when this error
458+
// arrives is a failure".
459+
if (
460+
entry.type === "custom" &&
461+
entry.customType === "openclaw:prompt-error"
462+
) {
463+
const errorProvider =
464+
typeof entry.data?.provider === "string" ? entry.data.provider : null;
465+
if (errorProvider) {
466+
currentProvider = errorProvider;
467+
}
468+
for (const index of pendingUserIndexes) {
469+
const message = userMessages[index];
470+
if (!message) {
471+
continue;
472+
}
473+
userMessages[index] = {
474+
id: message.id,
475+
timestampMs: message.timestampMs,
476+
createdAt: message.createdAt,
477+
providerName: errorProvider ?? message.providerName,
478+
channel: message.channel,
479+
state: "false",
480+
};
481+
}
482+
pendingUserIndexes.length = 0;
483+
continue;
484+
}
485+
450486
if (entry.type !== "message" || !entry.message) {
451487
continue;
452488
}
@@ -465,6 +501,7 @@ export class AnalyticsService {
465501
createdAt: entry.timestamp ?? null,
466502
providerName: currentProvider,
467503
channel: params.channel,
504+
state: "Success",
468505
});
469506
pendingUserIndexes.push(userMessages.length - 1);
470507
continue;
@@ -491,6 +528,7 @@ export class AnalyticsService {
491528
createdAt: message.createdAt,
492529
providerName,
493530
channel: message.channel,
531+
state: message.state,
494532
};
495533
}
496534
}

apps/web/src/layouts/workspace-layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,7 @@ function WorkspaceLayoutInner() {
963963
data-sidebar-growth-card="rewards"
964964
className="group mx-3 mb-2 flex items-center gap-3 rounded-[12px] border border-[#F5DFC0]/50 bg-gradient-to-br from-[#FFF8F0] via-[#FFFAF5] to-[#FFF5EB] px-3.5 py-3 shadow-[0_1px_3px_rgba(245,200,120,0.08)] transition-all duration-200 hover:border-[#F0D0A0]/60 hover:shadow-[0_2px_8px_rgba(245,200,120,0.15)]"
965965
onClick={() => {
966+
track("workspace_growth_rewards_click");
966967
track("workspace_rewards_click");
967968
track("workspace_sidebar_click", { target: "rewards" });
968969
}}
@@ -1049,6 +1050,7 @@ function WorkspaceLayoutInner() {
10491050
className="mt-2.5 flex w-full items-center justify-between border-t border-border/60 pt-2.5 text-[11px] font-medium text-text-secondary transition-colors hover:text-text-primary"
10501051
onClick={() => {
10511052
setShowBalancePopup(false);
1053+
track("workspace_click_usage_detail");
10521054
track("workspace_sidebar_click", {
10531055
target: "credits_popup_detail",
10541056
});

apps/web/src/lib/tracking.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import posthog, {
44
type Property,
55
} from "posthog-js";
66

7-
export type AnalyticsAuthSource = "welcome_page" | "settings";
7+
export type AnalyticsAuthSource = "welcome_page" | "settings" | "home";
88
export type AnalyticsChannel =
99
| "qqbot"
1010
| "dingtalk"
@@ -209,6 +209,9 @@ export function normalizeAuthSource(
209209
if (source === "settings") {
210210
return "settings";
211211
}
212+
if (source === "home") {
213+
return "home";
214+
}
212215
if (!source || source === "Landing" || source === "welcome_page") {
213216
return "welcome_page";
214217
}

0 commit comments

Comments
 (0)