-
-
Notifications
You must be signed in to change notification settings - Fork 735
Feature/welcome page ab test #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
f170418
3b967d0
65fe14a
c36bdda
ee09afd
8bebad1
6e8a2bd
c962f68
86219cc
f756e5d
7c6e0f2
dbc84e8
90b3f80
2af97d9
9dcb1c5
568a105
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { configManager } from '../config-manager.js'; | ||
|
|
||
| export interface ABTestConfig { | ||
| name: string; // Test name, used as config key prefix | ||
| variants: string[]; // e.g., ['control', 'treatment'] or ['A', 'B', 'C'] | ||
| } | ||
|
|
||
| export interface ABTestResult { | ||
| variant: string; | ||
| isNewAssignment: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Get or create A/B test assignment for current user | ||
| * Assignment is deterministic based on clientId and persisted in config | ||
| */ | ||
| export async function getABTestVariant(test: ABTestConfig): Promise<ABTestResult> { | ||
| const configKey = `abTest_${test.name}`; | ||
| const existing = await configManager.getValue(configKey); | ||
|
|
||
| if (existing !== undefined && test.variants.includes(existing)) { | ||
| return { variant: existing, isNewAssignment: false }; | ||
| } | ||
|
|
||
| // Assign based on clientId hash | ||
| const clientId = await configManager.getValue('clientId') || ''; | ||
| const hash = simpleHash(clientId + test.name); | ||
| const variantIndex = hash % test.variants.length; | ||
| const variant = test.variants[variantIndex]; | ||
|
|
||
| await configManager.setValue(configKey, variant); | ||
| return { variant, isNewAssignment: true }; | ||
| } | ||
|
|
||
| /** | ||
| * Simple hash function for deterministic assignment | ||
| */ | ||
| function simpleHash(str: string): number { | ||
| let hash = 0; | ||
| for (let i = 0; i < str.length; i++) { | ||
| hash = ((hash << 5) - hash) + str.charCodeAt(i); | ||
| hash |= 0; | ||
| } | ||
| return Math.abs(hash); | ||
| } | ||
|
|
||
| /** | ||
| * Check if user is in treatment group (convenience for 2-variant tests) | ||
| */ | ||
| export async function isInTreatment(testName: string): Promise<{ inTreatment: boolean; isNew: boolean }> { | ||
| const result = await getABTestVariant({ | ||
| name: testName, | ||
| variants: ['noOnboardingPage', 'sawOnboardingPage'] | ||
| }); | ||
| return { | ||
| inTreatment: result.variant === 'sawOnboardingPage', | ||
| isNew: result.isNewAssignment | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { execFile, spawn } from 'child_process'; | ||
| import os from 'os'; | ||
| import { logToStderr } from './logger.js'; | ||
|
|
||
| /** | ||
| * Open a URL in the default browser (cross-platform) | ||
| * Uses execFile/spawn with args array to avoid shell injection | ||
| */ | ||
| export async function openBrowser(url: string): Promise<void> { | ||
| const platform = os.platform(); | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| const callback = (error: Error | null) => { | ||
| if (error) { | ||
| logToStderr('error', `Failed to open browser: ${error.message}`); | ||
| reject(error); | ||
| } else { | ||
| logToStderr('info', `Opened browser to: ${url}`); | ||
| resolve(); | ||
| } | ||
| }; | ||
|
|
||
| switch (platform) { | ||
| case 'darwin': | ||
| execFile('open', [url], callback); | ||
| break; | ||
| case 'win32': | ||
| // Windows 'start' is a shell builtin, use spawn with shell but pass URL as separate arg | ||
| spawn('cmd', ['/c', 'start', '', url], { shell: false }).on('close', (code) => { | ||
| code === 0 ? resolve() : reject(new Error(`Exit code ${code}`)); | ||
| }); | ||
|
Comment on lines
+28
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Windows code path bypasses callback and misses success logging. The Windows implementation uses 🔎 Proposed fix to unify error handling case 'win32':
- // Windows 'start' is a shell builtin, use spawn with shell but pass URL as separate arg
- spawn('cmd', ['/c', 'start', '', url], { shell: false }).on('close', (code) => {
- code === 0 ? resolve() : reject(new Error(`Exit code ${code}`));
- });
+ // Windows 'start' is a shell builtin, requires cmd.exe
+ spawn('cmd', ['/c', 'start', '', url], { shell: false }).on('close', (code) => {
+ callback(code === 0 ? null : new Error(`Exit code ${code}`));
+ });
break;🤖 Prompt for AI Agents |
||
| break; | ||
| default: | ||
| execFile('xdg-open', [url], callback); | ||
| break; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Open the Desktop Commander welcome page | ||
| */ | ||
| export async function openWelcomePage(): Promise<void> { | ||
| const url = 'https://desktopcommander.app/welcome-v6/?ref=first-run'; | ||
| await openBrowser(url); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: wonderwhy-er/DesktopCommanderMCP
Length of output: 100
🏁 Script executed:
Repository: wonderwhy-er/DesktopCommanderMCP
Length of output: 8079
🏁 Script executed:
Repository: wonderwhy-er/DesktopCommanderMCP
Length of output: 354
Fix privacy policy URL before release.
The manifest version bump to 0.3 and privacy_policies addition align with the new features. However, the privacy policy URL
https://legal.desktopcommander.app/privacy_desktop_commander_mcpreturns a 404 error and is not accessible. Ensure the correct URL is provided or the endpoint is deployed before shipping this release.🤖 Prompt for AI Agents