sillsdev / sillsdev/languageforge-lexbox
LexBox web Mixpanel analytics
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 9
- Forks
- 8
- Avg merge
- 2d 13h
- Merged PRs (30d)
- 49
Description
LexBox Web Mixpanel Analytics Plan
Related: #2383 (FwLite analytics β team decisions and privacy patterns apply here too)
Context
- Scope: LexBox web app (
frontend/) plus backend Send/Receive (SyncReverseProxy/). Not FwLite (frontend/viewer/) or FwHeadless service-account sync jobs. - Issue #2383 is FwLite-focused, but its team decisions transfer well: opt-out in production, hard-off in dev, Simplified ID Merge identity, no linguistic/PII payloads, phased event rollout.
- Today: LexBox has OpenTelemetry for operational tracing (
frontend/src/lib/otel/) β user id is already attached to spans asapp.user.id. Product analytics is a separate concern; OTEL stays as-is. - User identity is already available: JWT β
LexAuthUser.id(stable Guid). This should be$user_idin Mixpanel, same as the FwLite spec recommends.
flowchart LR
subgraph client [LexBox SvelteKit Client]
Layout["+layout.svelte"]
Hooks["hooks.client.ts"]
Pages["Routes and mutations"]
ClientAnalytics["$lib/analytics"]
end
subgraph server [LexBox Backend]
Proxy["SyncReverseProxy Forward"]
ProxyEvents["ProxyEventsService"]
ServerAnalytics["IAnalyticsService"]
end
MP[Mixpanel]
Layout --> ClientAnalytics
Hooks --> ClientAnalytics
Pages --> ClientAnalytics
ClientAnalytics -->|"browser SDK or API proxy"| MP
Proxy --> ProxyEvents
ProxyEvents --> ServerAnalytics
ServerAnalytics -->|"mixpanel-csharp"| MP
Architecture (high level)
| Decision | Recommendation |
|---|---|
| SDK (web) | Mixpanel browser SDK (mixpanel-browser) for SvelteKit UI events |
| SDK (server) | mixpanel-csharp (same library as FLEx / #2383) for Send/Receive and any future server-side LexBox events |
| Token handling | Server-only token β never in client bundles. Web events via API proxy; S&R events emitted directly from SyncReverseProxy |
| Central modules | $lib/analytics/ (client) + shared IAnalyticsService in LexCore or LexBoxApi (server) |
| Instrumentation style | Explicit track() at funnel boundaries (login, project create) + sanitized route tracking for page views; S&R at completion hooks in ProxyEventsService |
| Mixpanel project | Separate project from FwLite, but use the same $user_id (Lexbox User Guid) so cross-product journeys can be joined later if desired |
Init hook points: hooks.client.ts (bootstrap) and authenticated +layout.svelte (re-identify() when JWT user loads). Auth events in login/+page.svelte, register/+page.svelte, logout route.
Consent and guardrails
Mirror #2383 decisions, adapted for web:
| Environment | Tracking |
|---|---|
Production (lexbox.org etc.) |
On by default, user can opt out |
Dev (pnpm run dev, local stack) |
Hard disabled β import.meta.env.DEV |
CI / Playwright (frontend/tests/) |
Hard disabled β env flag or hostname check |
Sandbox routes (routes/(unauthenticated)/sandbox/) |
Never track |
User control: Add a "Send usage statistics" toggle under Account settings (user/+page.svelte), persisted server-side (new user preference on LexAuthUser or a lightweight API). Call Mixpanel opt_out_tracking() when disabled.
Disclosure: Short notice on first visit (or in About/Privacy) β anonymous device ID before login, stable user Guid after login, link to what is never collected.
Identity (Simplified ID Merge)
Same model as docs/issues/2383-fwlite-analytics-spec.md:
| ID | When | Source |
|---|---|---|
$device_id |
Always (anonymous + authenticated) | Generated on first visit, localStorage |
$user_id |
After login/register success | LexAuthUser.id from JWT |
Lifecycle:
- Pre-login (login, register, forgot password, accept invitation): events with
$device_idonly - Login/register success:
identify(user.id)β next events include both IDs (triggers merge) - App load while authenticated: re-
identify()from layout load data - Logout:
reset()β new$device_id, clear$user_id(shared-device safety)
Never send: email, username, display name, tokens. $user_id Guid only.
Super properties (every event)
| Property | Source |
|---|---|
$device_id / $user_id |
Identity layer |
app_version |
APP_VERSION |
product |
lexbox_web (distinguishes from future FwLite events if projects merge) |
ui_language |
Active locale |
is_admin |
user.isAdmin |
feature_flags |
Enabled flags only (enum names, not custom data) |
Contextual (when applicable, never identifying):
project_id: LexboxProject.IdGuid β allowed for S&R events; still omit human-readableproject_codeproject_type:FwData|Crdt| etc. (enum from GraphQL)project_role:Manager|Editor|Observerorg_role: enumauth_method:password|google|bearer(S&R JWT)view_mode:grid|table(home project list)sr_protocol:hgweb|resumable(S&R only)
Send/Receive tracking (backend)
LexBox exposes Mercurial Send/Receive to FLEx and other FieldWorks clients via SyncReverseProxy. This is entirely server-side β no browser involved β but is a core collaboration signal worth tracking in the same Mixpanel project.
Where to hook
S&R traffic is proxied in ProxyKernel.Forward(), which already tags OTEL spans with app.project_code and app.send_receive. Completion is already detected in ProxyEventsService:
| Protocol | Completion signal | Meaning |
|---|---|---|
| hgweb | cmd=unbundle request succeeds |
Client pushed a changeset bundle to the server |
| resumable | Last chunk of pushBundleChunk (offset + chunksize >= bundlesize, status 200) |
Client finished uploading a resumable bundle |
These are the right places to fire send_receive_completed β they align with existing QueueProjectMetadataUpdate calls (metadata refresh after real data landed).
Identity on S&R requests: BasicAuthHandler resolves the caller to LexAuthUser (password, or bearer JWT with SendAndReceive scope from IntegrationController). $user_id = LexAuthUser.Id.
Project ID: Resolve projectCode β Project.Id Guid via ProjectService.LookupProjectId (cache lookups β S&R is hot path).
S&R events (Phase 1)
| Event | When | Key properties |
|---|---|---|
send_receive_completed |
ProxyEventsService completion hooks above |
$user_id, project_id, sr_protocol: hgweb | resumable |
send_receive_failed |
Proxy auth failure, ProjectLockedException, or non-2xx on completion request |
$user_id (if known), project_id (if resolved), failure_reason: unauthorized | project_locked | proxy_error |
Defer for later: per-request hg command tracking, pull/receive direction inference, bytes transferred, commit hashes, abandoned-transaction details (OTEL already tags app.abandoned_transaction_detected for ops).
Out of scope for S&R analytics: FwHeadless automated S&R during CRDT sync jobs (SyncHostedService) β service-account initiated, not end-user collaboration. Track only user-authenticated proxy traffic unless product later wants infra metrics separately.
S&R consent
S&R events must respect the same per-user opt-out preference as web events. Check User.SendUsageStatistics (or equivalent) before emitting; skip tracking if opted out. No anonymous S&R β callers are always authenticated.
S&R vs FwLite sync (#2383)
Issue #2383 defers FwLite CRDT sync tracking. LexBox Mercurial Send/Receive is a distinct, user-visible collaboration action and is explicitly in scope here.
What to track β event catalog by phase
Phase 1 β MVP funnels (ship first)
Answers: Do people sign up, create/join projects, and come back?
| Event | Trigger | Key properties |
|---|---|---|
session_started |
Client bootstrap | entry_route (sanitized route id, no params) |
page_viewed |
SvelteKit navigation | route_id (e.g. /(authenticated)/project/[project_code]), not project_code value |
login_started |
Login form submit or Google button | auth_method |
login_succeeded / login_failed |
login() result |
failure_reason: bad_credentials | locked | unknown |
register_started / register_succeeded / register_failed |
Register / accept-invitation flows | failure_reason enums (turnstile, account exists, etc.) |
logout |
Logout route | β |
project_create_started |
project/create submit |
β |
project_create_completed |
Create mutation result | outcome: created | join_request_sent | draft_saved | failed |
project_opened |
Project detail page load | project_type, user_role, is_draft, confidentiality β not code/name |
email_verification_completed |
Email verification callback | β |
send_receive_completed |
Backend: ProxyEventsService hgweb unbundle or resumable push complete |
$user_id, project_id, sr_protocol |
Activation milestone: first_project_created β fire once per user when first project_create_completed with outcome: created.
Collaboration milestone: first_send_receive_completed β fire once per user on first successful S&R.
Phase 2 β Engagement and collaboration
Answers: Are teams forming? Are project admins using key features?
| Event | Trigger |
|---|---|
project_list_viewed |
Home +page.svelte |
project_member_invited |
Add member modal success |
project_member_added |
Bulk add / direct add success |
ask_to_join_project |
Join request from create flow |
project_left / project_deleted |
Leave/delete confirmed |
org_created |
org/create |
org_member_invited |
Org member invite |
user_settings_updated |
Account settings save (which fields changed: locale, name β not values) |
open_in_flex_clicked |
OpenInFlexButton |
send_receive_url_copied |
Copy Send/Receive URL on project page (UI intent β complements backend send_receive_completed) |
fwlite_beta_requested |
wheresMyProject |
oauth_authorize_completed |
OAuth consent for third-party apps (authorize) |
Retention signals: segment on project_opened, project_create_completed, project_member_invited, send_receive_completed (weekly, by $user_id).
Phase 3 β Feature adoption (lower priority)
| Event | Question it answers |
|---|---|
project_backup_downloaded |
Are admins backing up before destructive ops? |
project_reset_completed |
Reset flow completion rate |
crdt_sync_ui_opened |
Interest in FwLite sync UI (not sync outcomes) |
admin_action |
Sparse admin-panel actions (user created, project approved) |
invitation_link_opened |
Invitation funnel drop-off |
Explicitly out of scope (never track)
Same privacy bar as #2383, plus web-specific items:
- Project names and codes (human-readable identifiers)
project_idand$user_idare allowed where needed for product analytics (S&R)- Member emails, usernames, display names
- Entry/linguistic content (LexBox web does not edit entries, but guard anyway)
- hg commit hashes, repo URLs, bundle contents, per-hg-command telemetry
- FwLite CRDT sync events (deferred per #2383; distinct from LexBox Mercurial S&R)
- GraphQL query variables, form field values
- Full URLs with query params containing identifiers
Funnel definitions
flowchart TD
Visit[session_started] --> Auth{authenticated?}
Auth -->|no| Register[register_succeeded]
Auth -->|yes| Home[project_list_viewed]
Register --> Verify[email_verification_completed]
Verify --> Create[project_create_completed]
Home --> Create
Create --> Collaborate[project_member_invited]
Collaborate --> Retained[project_opened on D7]
| Funnel | Steps | Product question |
|---|---|---|
| Signup activation | register_succeeded β email_verification_completed β first_project_created |
Is self-serve onboarding working? |
| Invitation activation | register_succeeded (invitation) β project_opened |
Do invited users land in a project? |
| Project creation | project_create_started β project_create_completed |
Where do create flows fail? |
| Collaboration | project_create_completed β project_member_invited |
Do creators invite teammates? |
| FLEx collaboration | open_in_flex_clicked or send_receive_url_copied β send_receive_completed |
Are web users actually syncing with FLEx? |
| Retention | first_project_created β project_opened on D7/D30 |
Do admins return? |
| S&R retention | first_send_receive_completed β send_receive_completed on D7/D30 |
Are collaborators syncing regularly? |
Implementation sequencing
- Mixpanel project setup β enable Simplified ID Merge; create dev/staging/prod projects or use env-based tokens
- Server analytics service β
mixpanel-csharpwrapper with Simplified ID Merge ($user_idonly for server events; no$device_idneeded); register inLexBoxApi+SyncReverseProxy - Analytics module + consent β
$lib/analytics, env gating, user opt-out preference (API + UI); server checks same preference - Identity wiring β login, register, logout, authenticated layout re-identify (client)
- Phase 1 events β session, auth, project create/open, activation milestones, S&R completion in
ProxyEventsService - Dashboards β signup activation, project creation, S&R adoption, D7 retention boards in Mixpanel
- Privacy docs β user-facing disclosure + internal event dictionary (reuse structure from
docs/issues/2383-fwlite-analytics-spec.md) - Phase 2/3 β iterate based on which product questions remain unanswered
Key files to touch (when implementing)
| Area | Files |
|---|---|
| Analytics core (client) | New frontend/src/lib/analytics/ |
| Analytics core (server) | New LexCore or LexBoxApi analytics service; wire into SyncReverseProxy DI |
| Send/Receive | ProxyEventsService.cs, ProxyKernel.cs |
| Bootstrap | hooks.client.ts, root +layout.svelte |
| Auth | lib/user.ts, login/register/logout routes |
| Projects | project/create/+page.svelte, project/[project_code]/+page.svelte |
| Settings | user/+page.svelte + backend preference endpoint |
| Config | frontend/.env pattern, deployment secrets, Mixpanel token in K8s secrets |
Open decisions (resolve before implementation)
- Server proxy vs direct SDK β proxy is safer for token; direct SDK is simpler. Recommend proxy unless latency is a concern.
- Opt-out storage β new
User.SendUsageStatisticsDB field vs client-only cookie (server field survives devices and matches FLEx pattern). - Shared vs separate Mixpanel project from FwLite β separate projects with shared
$user_idis the pragmatic default; merge later if cross-product dashboards are needed. - S&R failure instrumentation depth β track failures only at
ProxyKernel.Forwardboundary initially, or also per-protocol inProxyEventsService?
Implementation checklist
- Extend or wrap mixpanel-csharp for Simplified ID Merge on server; add browser SDK + optional API proxy for web
- Add
User.SendUsageStatistics(or equivalent) + account settings UI + dev-build hard disable - Wire identity: login, register, logout, authenticated layout re-identify
- Instrument Phase 1 web events +
send_receive_completed/send_receive_failedinProxyEventsService - Draft user-facing privacy disclosure + internal event dictionary
- Create Mixpanel boards: signup activation, project creation, S&R adoption, D7 retention
- Prioritize Phase 2/3 events after Phase 1 ships
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up β it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the related #2383 decisions, then inspect frontend/src/hooks.client.ts, the authenticated +layout.svelte, and SyncReverseProxy/Services/ProxyEventsService.cs. Review the listed authentication, project, consent, and Send/Receive hook points before choosing an implementation breakdown. Done means the agreed Mixpanel phases, opt-out rules, identity handling, and listed server and web events are implemented without sending prohibited data.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, typescript
- Domain
- analytics, full-stack
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100