-
-
Notifications
You must be signed in to change notification settings - Fork 737
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 10 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,108 @@ | ||
| import { configManager } from '../config-manager.js'; | ||
| import { featureFlagManager } from './feature-flags.js'; | ||
|
|
||
| /** | ||
| * A/B Test controlled feature flags | ||
| * | ||
| * Experiments are defined in remote feature flags JSON: | ||
| * { | ||
| * "flags": { | ||
| * "experiments": { | ||
| * "OnboardingPreTool": { | ||
| * "variants": ["noOnboardingPage", "showOnboardingPage"] | ||
| * } | ||
| * } | ||
| * } | ||
| * } | ||
| * | ||
| * Usage: | ||
| * if (await hasFeature('showOnboardingPage')) { ... } | ||
| */ | ||
|
|
||
| interface Experiment { | ||
| variants: string[]; | ||
| } | ||
|
|
||
| // Cache for variant assignments (loaded once per session) | ||
| const variantCache: Record<string, string> = {}; | ||
|
|
||
| /** | ||
| * Get experiments config from feature flags | ||
| */ | ||
| function getExperiments(): Record<string, Experiment> { | ||
| return featureFlagManager.get('experiments', {}); | ||
| } | ||
|
|
||
| /** | ||
| * Get user's variant for an experiment (cached, deterministic) | ||
| */ | ||
| async function getVariant(experimentName: string): Promise<string | null> { | ||
| const experiments = getExperiments(); | ||
| const experiment = experiments[experimentName]; | ||
| if (!experiment?.variants?.length) return null; | ||
|
|
||
| // Check cache | ||
| if (variantCache[experimentName]) { | ||
| return variantCache[experimentName]; | ||
| } | ||
|
|
||
| // Check persisted assignment | ||
| const configKey = `abTest_${experimentName}`; | ||
| const existing = await configManager.getValue(configKey); | ||
|
|
||
| if (existing && experiment.variants.includes(existing)) { | ||
| variantCache[experimentName] = existing; | ||
| return existing; | ||
| } | ||
|
|
||
| // New assignment based on clientId | ||
| const clientId = await configManager.getValue('clientId') || ''; | ||
| const hash = hashCode(clientId + experimentName); | ||
| const variantIndex = hash % experiment.variants.length; | ||
| const variant = experiment.variants[variantIndex]; | ||
|
|
||
| await configManager.setValue(configKey, variant); | ||
| variantCache[experimentName] = variant; | ||
| return variant; | ||
| } | ||
|
|
||
| /** | ||
| * Check if a feature (variant name) is enabled for current user | ||
| */ | ||
| export async function hasFeature(featureName: string): Promise<boolean> { | ||
| const experiments = getExperiments(); | ||
|
|
||
| for (const [expName, experiment] of Object.entries(experiments)) { | ||
| if (experiment.variants?.includes(featureName)) { | ||
| const variant = await getVariant(expName); | ||
| return variant === featureName; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Get all A/B test assignments for analytics (reads from config) | ||
| */ | ||
| export async function getABTestAssignments(): Promise<Record<string, string>> { | ||
| const experiments = getExperiments(); | ||
| const assignments: Record<string, string> = {}; | ||
|
|
||
| for (const expName of Object.keys(experiments)) { | ||
| const configKey = `abTest_${expName}`; | ||
| const variant = await configManager.getValue(configKey); | ||
| if (variant) { | ||
| assignments[`ab_${expName}`] = variant; | ||
| } | ||
| } | ||
| return assignments; | ||
| } | ||
|
|
||
| function hashCode(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); | ||
| } |
| 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