Skip to content

Add workspace and runtime analytics events#411

Merged
Siri-Ray merged 4 commits intomainfrom
feat/analytics-workspace-and-runtime-events
Mar 23, 2026
Merged

Add workspace and runtime analytics events#411
Siri-Ray merged 4 commits intomainfrom
feat/analytics-workspace-and-runtime-events

Conversation

@Siri-Ray
Copy link
Copy Markdown
Contributor

@Siri-Ray Siri-Ray commented Mar 23, 2026

Summary

  • add the new workspace/auth/skills/model/channel analytics events across the Nexu app while dual-sending existing event names where needed
  • add controller-side analytics polling to emit session_start, user_message_sent, and skill_use from OpenClaw runtime transcripts
  • keep event properties aligned with the agreed lowercase schema and normalized channel/auth/skill source values

Validation

  • pnpm typecheck
  • pnpm lint
  • pnpm test

Notes

  • workspace_provider_model_enable is not included because the current product does not expose a real per-model enable action
  • landing events are tracked in a separate PR for nexu-landing

Summary by CodeRabbit

  • New Features

    • App-wide analytics added: tracks workspace navigation, sidebar and conversation clicks, channel connect/disconnect, model changes, skill install/uninstall, auth/signup/login success, GitHub CTAs, welcome option selections, channel chat links, and related UI actions.
  • Chores

    • Background analytics service and polling loop introduced with persistent send-state and new environment settings to support reliable event delivery.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Adds a new backend AnalyticsService with polling, persisted dedupe/send-state and Amplitude integration; exposes Amplitude keys and analyticsStatePath in controller env; starts/stops an analytics background loop in container lifecycle; and instruments many frontend components/pages with tracking events and helpers.

Changes

Cohort / File(s) Summary
Backend Analytics Service
apps/controller/src/services/analytics-service.ts
New exported AnalyticsService class with poll() that reads sessions/transcripts, infers providers/skill uses, deduplicates events, sends Amplitude events (user_message_sent, skill_use, session_start), and persists send-state to env.analyticsStatePath.
Backend Lifecycle & Env
apps/controller/src/app/container.ts, apps/controller/src/app/env.ts, apps/controller/src/runtime/loops.ts
Adds analyticsService to ControllerContainer, exposes AMPLITUDE_API_KEY/VITE_AMPLITUDE_API_KEY and analyticsStatePath on env, introduces startAnalyticsLoop(...) and wires start/stop into container lifecycle.
Frontend Tracking Core
apps/web/src/lib/tracking.ts
Adds analytics types and mapping helpers (AnalyticsAuthSource, AnalyticsChannel, AnalyticsSidebarTarget, AnalyticsGitHubSource, AnalyticsSkillSource) and functions normalizeAuthSource, normalizeChannel, mapInstalledSkillSource.
Frontend: Auth & Welcome
apps/web/src/pages/auth.tsx, apps/web/src/pages/welcome.tsx
Normalizes auth source for email/OAuth flows and emits login_success/signup_success when source is present; tracks welcome option clicks.
Frontend: Channel Setup & Home
apps/web/src/components/channel-connect-modal.tsx, apps/web/src/components/channel-setup/...
apps/web/src/pages/channels.tsx, apps/web/src/pages/home.tsx
Adds tracking for channel config submit/connect/disconnect and chat-open clicks (workspace_channel_config_submit, workspace_channel_connect_click, workspace_channel_disconnect_click, workspace_chat_in_im_click).
Frontend: Model & Provider Tracking
apps/web/src/components/inline-model-selector.tsx, apps/web/src/components/model-picker-dropdown.tsx, apps/web/src/pages/models.tsx
Adds provider-derivation helper and emits model/provider analytics (workspace_change_model_change, workspace_change_model_click, workspace_configure_model_provider_click) and BYOK provider events (workspace_provider_check, workspace_provider_save).
Frontend: Navigation & Sidebar
apps/web/src/layouts/workspace-layout.tsx
Adds sidebar/help/docs/logout/GitHub click tracking and workspace_sidebar_click variants; removes the initial workspace_view emission.
Frontend: Skills & Import
apps/web/src/pages/skills.tsx, apps/web/src/components/skills/import-skill-modal.tsx
Passes skillSource to SkillCard and emits workspace_skill_enable/workspace_skill_disable on install/uninstall; tracks successful skill import with slug/name.
Frontend: Small UI Clicks
apps/web/src/components/brand-rail.tsx, apps/web/src/pages/sessions.tsx, apps/web/src/pages/home.tsx
Adds GitHub click and conversation/chat link tracking across brand rail, sessions, and home CTAs.
Docs & Theme formatting
docs/.vitepress/config.ts, docs/.vitepress/theme/*, docs/.vitepress/theme/custom.css, docs/.vitepress/theme/index.ts
Formatting and minor refactors in VitePress theme files; one rename in ThemeToggle script may create a template binding mismatch to verify.

Sequence Diagram

sequenceDiagram
    participant SessionsRuntime as SessionsRuntime
    participant FileSystem as File System
    participant AnalyticsService as AnalyticsService
    participant Amplitude as Amplitude API

    loop Polling Loop (every runtimeSyncIntervalMs)
        AnalyticsService->>SessionsRuntime: listSessions()
        SessionsRuntime-->>AnalyticsService: sessions list
        AnalyticsService->>FileSystem: read sessions.json, transcripts, skill ledger
        FileSystem-->>AnalyticsService: transcripts & metadata
        AnalyticsService->>AnalyticsService: parse entries, infer provider/skill candidates, dedupe
        AnalyticsService->>Amplitude: POST analytics events (user_message_sent, skill_use, session_start)
        Amplitude-->>AnalyticsService: response / errors
        AnalyticsService->>FileSystem: persist send-state (analyticsStatePath)
        FileSystem-->>AnalyticsService: state saved
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • lefarcen
  • qiongyu1999

Poem

🐰 I hopped through logs and transcripts deep,

I counted clicks and skills while others sleep,
From session starts to tools the assistants use,
I tucked the events in files and chased the clues,
A tiny rabbit mapped each hopping beat. 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description provides a clear summary, validation steps, and relevant notes, though it deviates from the template structure by omitting sections like 'What', 'Why', 'How', 'Affected areas', and the checklist. Consider following the repository's PR template more closely by filling out all sections (What, Why, How, Affected areas, Checklist) to provide comprehensive context for reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add workspace and runtime analytics events' accurately summarizes the main change—comprehensive analytics instrumentation across the application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/analytics-workspace-and-runtime-events

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…ace-and-runtime-events

# Conflicts:
#	apps/web/src/pages/skills.tsx
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Mar 23, 2026

Deploying nexu-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3f8b410
Status: ✅  Deploy successful!
Preview URL: https://d179bbda.nexu-docs.pages.dev
Branch Preview URL: https://feat-analytics-workspace-and.nexu-docs.pages.dev

View logs

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/src/components/channel-setup/feishu-setup-view.tsx (1)

137-166: ⚠️ Potential issue | 🟡 Minor

Thrown connect errors are not reported as failed submissions.

If postApiV1ChannelsFeishuConnect throws, the catch path does not emit workspace_channel_config_submit with success: false.

Suggested fix
-    } catch {
+    } catch {
+      track("workspace_channel_config_submit", {
+        channel: "feishu",
+        success: false,
+      });
       toast.error(t("feishuSetup.connectFailed"));
     } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/channel-setup/feishu-setup-view.tsx` around lines 137
- 166, The catch block in handleConnect doesn't report a failed submission when
postApiV1ChannelsFeishuConnect throws; update the catch to call
track("workspace_channel_config_submit", { channel: "feishu", success: false })
before showing the toast (and optionally include any error message), so failures
thrown by postApiV1ChannelsFeishuConnect are recorded the same way as error
responses.
apps/web/src/components/channel-setup/slack-oauth-view.tsx (1)

206-237: ⚠️ Potential issue | 🟡 Minor

Manual-connect exception path is not tracked as failed submit.

When postApiV1ChannelsSlackConnect throws, Line 233 only shows a toast, so workspace_channel_config_submit misses failure telemetry.

Suggested fix
-    } catch {
+    } catch {
+      track("workspace_channel_config_submit", {
+        channel: "slack",
+        success: false,
+      });
       toast.error(t("slackSetup.connectFailed"));
     } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/channel-setup/slack-oauth-view.tsx` around lines 206
- 237, In handleManualConnect, the catch block for postApiV1ChannelsSlackConnect
does not emit the failure telemetry, so add a
track("workspace_channel_config_submit", { channel: "slack", success: false })
inside the catch before showing the toast (and optionally include error info if
available); reference the existing track calls in the success and error branches
to ensure symmetry with the success path and keep setConnecting(false) in the
finally as-is.
apps/web/src/components/channel-setup/discord-setup-view.tsx (1)

64-95: ⚠️ Potential issue | 🟡 Minor

Exception path is missing success: false tracking.

If postApiV1ChannelsDiscordConnect throws, Line 91 only toasts and the submit-failure event is never emitted.

Suggested fix
-    } catch {
+    } catch {
+      track("workspace_channel_config_submit", {
+        channel: "discord",
+        success: false,
+      });
       toast.error(t("discordSetup.connectFailed"));
     } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/channel-setup/discord-setup-view.tsx` around lines 64
- 95, The catch block in handleConnect (which calls
postApiV1ChannelsDiscordConnect) fails to emit the submit-failure analytics; add
a track(...) call there mirroring the error path (e.g.,
track("workspace_channel_config_submit", { channel: "discord", success: false
})) before showing the toast so failures from thrown exceptions are tracked;
place it at the start of the catch block in discord-setup-view.tsx so existing
toast.error and finally setConnecting(false) behavior remain unchanged.
🧹 Nitpick comments (2)
apps/controller/src/runtime/loops.ts (1)

111-118: Make the analytics loop stoppable without waiting for the next interval.

stopAnalyticsLoop() only flips stopped; it does not cancel the pending sleep(). Cleanup can therefore lag by up to runtimeSyncIntervalMs, and that timer can keep the process alive longer than expected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/runtime/loops.ts` around lines 111 - 118, The loop
currently sets stopped = true in the returned cleanup but leaves a pending
sleep(params.env.runtimeSyncIntervalMs) unresolved, so cleanup can wait the full
interval; change the loop to use a cancellable wait (e.g., race sleep(...) with
a stop promise or use an AbortController) inside run so the sleep resolves
immediately when stopping. Concretely: inside the scope that defines run and
stopped, create a stopResolve function or AbortController and use
Promise.race([sleep(...), stopPromise]) (or an abortableSleep that listens to
the controller) instead of awaiting sleep() directly; then call stopResolve()
(or controller.abort()) in the returned cleanup closure so the pending wait is
cancelled and run exits without waiting for the interval. Ensure references to
run, stopped, and sleep are updated accordingly.
apps/controller/src/services/analytics-service.ts (1)

160-175: Avoid reparsing full transcript history on every poll.

Each pass re-reads every .jsonl transcript plus the sibling sessions.json for every session, so controller cost grows with total retained history instead of new activity. Persist a per-session file offset/mtime and cache sessions.json per directory to keep the loop bounded.

Also applies to: 305-375

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/analytics-service.ts` around lines 160 - 175,
The loop in analytics-service (using sessionsRuntime.listSessions(),
readTranscript(), readResolvedSkills(), and readSkillLedgerSources()) re-reads
full transcript and sessions.json for every session on each poll; fix by
persisting and checking per-session metadata (file offset and mtime) and caching
directory-level sessions.json to avoid re-parsing unchanged files: update the
session processing to load a small per-session state store (lastReadOffset,
lastMtime, lastResolvedSkillsHash) and only call
readTranscript/readResolvedSkills for files that changed since last poll, and
add a cache keyed by session directory for the sessions.json content read by
readSkillLedgerSources/readResolvedSkills so repeated lookups in the same dir
reuse the parsed value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/src/app/env.ts`:
- Line 136: The current amplitudeApiKey assignment uses nullish coalescing
(parsed.AMPLITUDE_API_KEY ?? parsed.VITE_AMPLITUDE_API_KEY) which treats an
empty string as set; change it so an empty/whitespace AMPLITUDE_API_KEY is
considered unset and falls back to VITE_AMPLITUDE_API_KEY. Locate the
amplitudeApiKey assignment and replace the nullish-coalescing logic with a check
that treats parsed.AMPLITUDE_API_KEY as unset when it's undefined, null, or only
whitespace (e.g., use a trimmed truthiness check or explicit empty-string test)
so parsed.VITE_AMPLITUDE_API_KEY is used as the fallback.

In `@apps/controller/src/services/analytics-service.ts`:
- Around line 143-144: The dedupe sets sentUserMessageIds and sentSkillUseIds
are global but analyzeSession() constructs fallback ids like
assistant:${tool}:${index} or uses bare entry.id that can collide across
sessions; update analyzeSession() to prefix all generated/stored event ids with
the session.id (e.g., use `${session.id}:${entry.id}` or
`${session.id}:assistant:${tool}:${index}`) before checking/inserting into
sentUserMessageIds and sentSkillUseIds so deduping is session-scoped; apply the
same session-prefixed id logic wherever those sets are accessed/updated
(including the code paths around lines noted: 165-181, 442-447, 493-496).
- Around line 13-17: The AnalyticsState type uses a single boolean
sessionStartSent which prevents emitting session_start more than once globally;
change AnalyticsState to track session-scoped sent flags (e.g. replace
sessionStartSent with sentSessionStartIds: Record<string, boolean> or
Set<string>) and update all code paths that check/set sessionStartSent (and any
logic that deduplicates by that flag near the handlers referenced) to use a
session identifier (session id or first user-message id) as the key when
deciding to emit session_start; also update any persisted state reads/writes and
the related arrays sentUserMessageIds and sentSkillUseIds to follow the same
session-scoped approach so each new session can send its own session_start
without being blocked by a global flag.
- Around line 377-405: readSkillLedgerSources currently only returns installed
skills from the live ledger which causes resolveSkillSource to fall back to
"builtin" for skills that existed in a session snapshot; update the logic so the
per-session snapshot is authoritative by checking the resolved snapshot first
(use readResolvedSkills() data/its returned map) when resolving a skill source
(in resolveSkillSource or wherever skill_source is determined), and only fall
back to the ledger produced by readSkillLedgerSources() or to "builtin" if
neither snapshot nor ledger contains the slug; ensure you reference the existing
functions readResolvedSkills(), readSkillLedgerSources(), and resolveSkillSource
when implementing this fallback ordering.
- Around line 551-567: The Amplitude POST fetch in analytics-service.ts
currently has no explicit deadline; wrap the call with
AbortSignal.timeout(30000) (or 60000 for 60s) and pass the resulting signal in
the fetch options (add signal) so the poll loop can't be blocked by a slow
Amplitude response; update the fetch invocation that constructs the body with
this.env.amplitudeApiKey to include the signal and handle the abort/timeout
error (e.g., catch the AbortError and treat it as a non-fatal timeout) so the
poll iteration can continue.

In `@apps/web/src/components/channel-connect-modal.tsx`:
- Around line 155-162: The cancel tracking fires while a submit is still in
flight; update handleClose to only call track("workspace_channel_config_cancel",
...) when not submittedSuccessfully AND not loading (e.g., if
(!submittedSuccessfully && !loading) { track(...) }), and add loading to the
useCallback dependency array so the callback uses the current in-flight state;
keep the existing onClose() call unchanged.

In `@apps/web/src/pages/auth.tsx`:
- Around line 209-218: The legacy dual-sent tracking calls are using the raw
authSourceParam instead of the normalized value; update the track calls that
currently pass authSourceParam (e.g., the first track call that sends
"login_email_success"/"signup_email_success" and the provider-based
"*_${provider}_success" calls) to pass normalizedSource ?? "Landing" (or similar
default) so both the legacy and new events use
normalizeAuthSource(authSourceParam); locate usages around the blocks
referencing normalizeAuthSource, normalizedSource, isLogin, and track (also
apply the same change in the other duplicated blocks noted) and replace the raw
authSourceParam with normalizedSource for consistency.

In `@apps/web/src/pages/home.tsx`:
- Around line 867-870: Create a shared connect handler (e.g.,
handleChannelConnect) that receives the channel id/object, calls
normalizeChannel and emits track("workspace_channel_connect_click", { channel })
when normalizeChannel returns a value, and then performs the existing setup/open
flow; replace the two inline usages (the secondary grid block that currently
calls normalizeChannel + track and the primary onboarding cards that open the
same setup flow) to both call handleChannelConnect so the primary first-run
cards are tracked through the same funnel.

In `@apps/web/src/pages/models.tsx`:
- Around line 85-95: getProviderIdFromModelId currently falls back to using the
first path segment (or whole modelId) as a provider, causing bogus provider
values; change the fallback so that if no matching entry is found in models you
only extract and return a provider when modelId contains an explicit separator
(e.g., contains "/"), otherwise return null. Update the function
getProviderIdFromModelId (parameters models and modelId) to check for a slash
before splitting and returning the provider_name, and ensure it returns null for
bare model IDs that aren't present in models.

---

Outside diff comments:
In `@apps/web/src/components/channel-setup/discord-setup-view.tsx`:
- Around line 64-95: The catch block in handleConnect (which calls
postApiV1ChannelsDiscordConnect) fails to emit the submit-failure analytics; add
a track(...) call there mirroring the error path (e.g.,
track("workspace_channel_config_submit", { channel: "discord", success: false
})) before showing the toast so failures from thrown exceptions are tracked;
place it at the start of the catch block in discord-setup-view.tsx so existing
toast.error and finally setConnecting(false) behavior remain unchanged.

In `@apps/web/src/components/channel-setup/feishu-setup-view.tsx`:
- Around line 137-166: The catch block in handleConnect doesn't report a failed
submission when postApiV1ChannelsFeishuConnect throws; update the catch to call
track("workspace_channel_config_submit", { channel: "feishu", success: false })
before showing the toast (and optionally include any error message), so failures
thrown by postApiV1ChannelsFeishuConnect are recorded the same way as error
responses.

In `@apps/web/src/components/channel-setup/slack-oauth-view.tsx`:
- Around line 206-237: In handleManualConnect, the catch block for
postApiV1ChannelsSlackConnect does not emit the failure telemetry, so add a
track("workspace_channel_config_submit", { channel: "slack", success: false })
inside the catch before showing the toast (and optionally include error info if
available); reference the existing track calls in the success and error branches
to ensure symmetry with the success path and keep setConnecting(false) in the
finally as-is.

---

Nitpick comments:
In `@apps/controller/src/runtime/loops.ts`:
- Around line 111-118: The loop currently sets stopped = true in the returned
cleanup but leaves a pending sleep(params.env.runtimeSyncIntervalMs) unresolved,
so cleanup can wait the full interval; change the loop to use a cancellable wait
(e.g., race sleep(...) with a stop promise or use an AbortController) inside run
so the sleep resolves immediately when stopping. Concretely: inside the scope
that defines run and stopped, create a stopResolve function or AbortController
and use Promise.race([sleep(...), stopPromise]) (or an abortableSleep that
listens to the controller) instead of awaiting sleep() directly; then call
stopResolve() (or controller.abort()) in the returned cleanup closure so the
pending wait is cancelled and run exits without waiting for the interval. Ensure
references to run, stopped, and sleep are updated accordingly.

In `@apps/controller/src/services/analytics-service.ts`:
- Around line 160-175: The loop in analytics-service (using
sessionsRuntime.listSessions(), readTranscript(), readResolvedSkills(), and
readSkillLedgerSources()) re-reads full transcript and sessions.json for every
session on each poll; fix by persisting and checking per-session metadata (file
offset and mtime) and caching directory-level sessions.json to avoid re-parsing
unchanged files: update the session processing to load a small per-session state
store (lastReadOffset, lastMtime, lastResolvedSkillsHash) and only call
readTranscript/readResolvedSkills for files that changed since last poll, and
add a cache keyed by session directory for the sessions.json content read by
readSkillLedgerSources/readResolvedSkills so repeated lookups in the same dir
reuse the parsed value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 21eb1306-b931-4890-b54c-1cef483f1878

📥 Commits

Reviewing files that changed from the base of the PR and between 458c3dc and fec775b.

📒 Files selected for processing (20)
  • apps/controller/src/app/container.ts
  • apps/controller/src/app/env.ts
  • apps/controller/src/runtime/loops.ts
  • apps/controller/src/services/analytics-service.ts
  • apps/web/src/components/brand-rail.tsx
  • apps/web/src/components/channel-connect-modal.tsx
  • apps/web/src/components/channel-setup/discord-setup-view.tsx
  • apps/web/src/components/channel-setup/feishu-setup-view.tsx
  • apps/web/src/components/channel-setup/slack-oauth-view.tsx
  • apps/web/src/components/inline-model-selector.tsx
  • apps/web/src/components/model-picker-dropdown.tsx
  • apps/web/src/layouts/workspace-layout.tsx
  • apps/web/src/lib/tracking.ts
  • apps/web/src/pages/auth.tsx
  • apps/web/src/pages/channels.tsx
  • apps/web/src/pages/home.tsx
  • apps/web/src/pages/models.tsx
  • apps/web/src/pages/sessions.tsx
  • apps/web/src/pages/skills.tsx
  • apps/web/src/pages/welcome.tsx

Comment thread apps/controller/src/app/env.ts Outdated
Comment thread apps/controller/src/services/analytics-service.ts
Comment thread apps/controller/src/services/analytics-service.ts
Comment thread apps/controller/src/services/analytics-service.ts
Comment thread apps/controller/src/services/analytics-service.ts
Comment thread apps/web/src/components/channel-connect-modal.tsx Outdated
Comment thread apps/web/src/pages/auth.tsx
Comment thread apps/web/src/pages/home.tsx
Comment thread apps/web/src/pages/models.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/skills/import-skill-modal.tsx`:
- Around line 86-88: The analytics call inside import-skill-modal.tsx is sending
a raw local filename via selectedFile.name as a fallback; update the
track("workspace_skill_enable", ...) invocation to avoid any client filenames by
removing the selectedFile.name fallback and instead send only the
server-provided result.slug (or a fixed safe constant like "unknown_skill" or
"local_skill" when result.slug is falsy). Locate the track call and replace the
name payload to use result.slug ?? "unknown_skill" (or just result.slug) so no
local filenames are included in analytics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1b4b030-2a9e-46aa-9626-32d238a076b8

📥 Commits

Reviewing files that changed from the base of the PR and between fec775b and 3558202.

📒 Files selected for processing (2)
  • apps/web/src/components/skills/import-skill-modal.tsx
  • apps/web/src/pages/skills.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/pages/skills.tsx

Comment thread apps/web/src/components/skills/import-skill-modal.tsx
lefarcen
lefarcen previously approved these changes Mar 23, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/channel-setup/feishu-setup-view.tsx (1)

151-166: ⚠️ Potential issue | 🟠 Major

Don't let post-success UI errors emit a failed submit event.

onConnected() still runs inside the same try. If it throws after the success track at Line 151, the catch at Line 163 emits workspace_channel_config_submit again with success: false and shows a failure toast after the success toast. Keep the analytics try/catch scoped to postApiV1ChannelsFeishuConnect, or move the post-success UI work outside it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/channel-setup/feishu-setup-view.tsx` around lines 151
- 166, The try/catch currently wraps both the API call and the post-success
UI/analytics, so if onConnected() or UI code throws after a successful API call
it incorrectly records a failed submit; refactor by limiting the try/catch to
the API call (postApiV1ChannelsFeishuConnect) and any analytics tracking (track
calls), then run toast.success, identify, and onConnected() outside that try
block (or add a separate try around analytics only) so that a post-success UI
error cannot trigger the failure branch that emits
workspace_channel_config_submit with success: false.
♻️ Duplicate comments (1)
apps/controller/src/services/analytics-service.ts (1)

13-17: ⚠️ Potential issue | 🟠 Major

Track session_start per session, not globally.

This is still a single boolean, so after the first successful session_start, every later transcript is skipped. Persist a session-scoped sent set keyed by session.id (or the first user-message id) instead.

Also applies to: 163-164, 184-193, 240-251

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/services/analytics-service.ts` around lines 13 - 17,
AnalyticsState currently uses a single boolean sessionStartSent which causes a
single global session_start to block all later sessions; change AnalyticsState
to track session-scoped state (e.g., sessionStartSentBySession: Record<string,
boolean> and sentUserMessageIdsBySession: Record<string, string[]> and
sentSkillUseIdsBySession: Record<string, string[]>), key entries by session.id
(or first user-message id) and update the logic that checks/sets
sessionStartSent, sentUserMessageIds, and sentSkillUseIds to read/write the
per-session maps (ensure removal/cleanup when sessions end or expire); update
any functions referencing sessionStartSent, sentUserMessageIds, or
sentSkillUseIds to use the session-keyed maps instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/src/app/env.ts`:
- Around line 136-137: The amplitudeApiKey assignment currently only trims
parsed.AMPLITUDE_API_KEY but leaves parsed.VITE_AMPLITUDE_API_KEY untrimmed;
update the amplitudeApiKey expression in env.ts (the amplitudeApiKey
export/field) to apply .trim() and emptiness handling to both sources (e.g., use
parsed.AMPLITUDE_API_KEY?.trim() || parsed.VITE_AMPLITUDE_API_KEY?.trim()) so a
whitespace-only VITE_AMPLITUDE_API_KEY doesn't produce a truthy invalid key.

In `@apps/controller/src/services/analytics-service.ts`:
- Around line 203-213: The poll() flow is adding dedupe state (calls like
this.sentUserMessageIds.add(...), this.sentSkillUseIds.add(...), and setting
this.state.sessionStartSent = true) immediately after calling sendEvent(), but
sendEvent() can fail and returns/logs on errors; only mark events as sent when
sendEvent actually succeeded. Update sendEvent callers in analytics-service (the
spots around the user_message_sent, skill_use, and session_start flows) to check
the boolean/result returned by sendEvent() and only execute
this.sentUserMessageIds.add(...), this.sentSkillUseIds.add(...), or set
this.state.sessionStartSent = true when sendEvent() indicates success; keep the
rest of the logic unchanged so transient Amplitude failures do not permanently
drop events.
- Around line 324-375: readResolvedSkills currently iterates
Object.values(parsed) and merges resolvedSkills from all sessions; change its
signature (readResolvedSkills) to accept the current sessionId and only read the
snapshot for that key from the parsed sessions JSON (use parsed[sessionId]
instead of Object.values(parsed)), then validate that entry's skillsSnapshot and
resolvedSkills (resolvedSkills, name, filePath, source) exactly as before;
update callers to pass the session.id; reference sessionsJsonPath, parsed,
snapshot, and resolvedSkills when making this selective lookup and preserving
existing type/validation checks.

In `@apps/web/src/components/channel-connect-modal.tsx`:
- Around line 215-229: The catch/rejection path of the Channel connect flow
isn't reporting failures to analytics — update the error-handling so that
whenever a postApiV1Channels*Connect call rejects you call
track("workspace_channel_config_submit", { channel: channelType, success: false
}) before showing the toast (i.e., add the same track call used in the 409/error
branch into the catch/rejection branch where the toast is currently shown),
ensuring the channelType value used matches the existing variable and keep the
existing toast behavior.

In `@apps/web/src/components/skills/import-skill-modal.tsx`:
- Around line 85-89: After successfully calling
importMutation.mutateAsync(selectedFile) (the const result = await
importMutation.mutateAsync(selectedFile) block), guard the analytics call
track(...) so it cannot throw and block the success UX: wrap the
track("workspace_skill_enable", { name: result.slug ?? "unknown_skill",
skill_source: "custom" }) call in its own try/catch (or Promise.catch if async)
and swallow/log analytics errors, ensuring the subsequent success handling
(closing the modal/updating state) always runs regardless of track failures.

---

Outside diff comments:
In `@apps/web/src/components/channel-setup/feishu-setup-view.tsx`:
- Around line 151-166: The try/catch currently wraps both the API call and the
post-success UI/analytics, so if onConnected() or UI code throws after a
successful API call it incorrectly records a failed submit; refactor by limiting
the try/catch to the API call (postApiV1ChannelsFeishuConnect) and any analytics
tracking (track calls), then run toast.success, identify, and onConnected()
outside that try block (or add a separate try around analytics only) so that a
post-success UI error cannot trigger the failure branch that emits
workspace_channel_config_submit with success: false.

---

Duplicate comments:
In `@apps/controller/src/services/analytics-service.ts`:
- Around line 13-17: AnalyticsState currently uses a single boolean
sessionStartSent which causes a single global session_start to block all later
sessions; change AnalyticsState to track session-scoped state (e.g.,
sessionStartSentBySession: Record<string, boolean> and
sentUserMessageIdsBySession: Record<string, string[]> and
sentSkillUseIdsBySession: Record<string, string[]>), key entries by session.id
(or first user-message id) and update the logic that checks/sets
sessionStartSent, sentUserMessageIds, and sentSkillUseIds to read/write the
per-session maps (ensure removal/cleanup when sessions end or expire); update
any functions referencing sessionStartSent, sentUserMessageIds, or
sentSkillUseIds to use the session-keyed maps instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea140632-ffc0-425c-b486-358c0a1a2742

📥 Commits

Reviewing files that changed from the base of the PR and between 3558202 and 78dd4bb.

📒 Files selected for processing (10)
  • apps/controller/src/app/env.ts
  • apps/controller/src/services/analytics-service.ts
  • apps/web/src/components/channel-connect-modal.tsx
  • apps/web/src/components/channel-setup/discord-setup-view.tsx
  • apps/web/src/components/channel-setup/feishu-setup-view.tsx
  • apps/web/src/components/channel-setup/slack-oauth-view.tsx
  • apps/web/src/components/skills/import-skill-modal.tsx
  • apps/web/src/pages/auth.tsx
  • apps/web/src/pages/home.tsx
  • apps/web/src/pages/models.tsx
✅ Files skipped from review due to trivial changes (2)
  • apps/web/src/components/channel-setup/discord-setup-view.tsx
  • apps/web/src/pages/home.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/components/channel-setup/slack-oauth-view.tsx
  • apps/web/src/pages/models.tsx

Comment thread apps/controller/src/app/env.ts
Comment thread apps/controller/src/services/analytics-service.ts
Comment thread apps/controller/src/services/analytics-service.ts
Comment thread apps/controller/src/services/analytics-service.ts
Comment thread apps/web/src/components/channel-connect-modal.tsx
Comment thread apps/web/src/components/skills/import-skill-modal.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/.vitepress/theme/ThemeToggle.vue`:
- Around line 7-9: The injected toggle handler is named _toggleAppearance but
the template calls toggleAppearance, causing an undefined click handler; fix by
making the names consistent—either rename the injected constant
_toggleAppearance to toggleAppearance (update the inject call that currently
uses _toggleAppearance) or change the template to call _toggleAppearance—ensure
the inject("toggle-appearance", ...) result and the template click handler
reference the same identifier.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 702878ca-d3bf-4407-ba4d-85456cdc4155

📥 Commits

Reviewing files that changed from the base of the PR and between 78dd4bb and 5de0a9a.

📒 Files selected for processing (6)
  • docs/.vitepress/config.ts
  • docs/.vitepress/theme/GitHubStars.vue
  • docs/.vitepress/theme/LanguageSwitcher.vue
  • docs/.vitepress/theme/ThemeToggle.vue
  • docs/.vitepress/theme/custom.css
  • docs/.vitepress/theme/index.ts
✅ Files skipped from review due to trivial changes (5)
  • docs/.vitepress/theme/index.ts
  • docs/.vitepress/config.ts
  • docs/.vitepress/theme/GitHubStars.vue
  • docs/.vitepress/theme/custom.css
  • docs/.vitepress/theme/LanguageSwitcher.vue

Comment thread docs/.vitepress/theme/ThemeToggle.vue Outdated
@Siri-Ray Siri-Ray force-pushed the feat/analytics-workspace-and-runtime-events branch from 5de0a9a to 8bb4e70 Compare March 23, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants