@amplitude/wizard 1.0.0-beta.0 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/bin.js +42 -8
  2. package/dist/package.json +2 -1
  3. package/dist/src/lib/agent-interface.js +10 -22
  4. package/dist/src/lib/agent-runner.js +4 -6
  5. package/dist/src/lib/commandments.js +1 -1
  6. package/dist/src/lib/constants.d.ts +0 -3
  7. package/dist/src/lib/constants.js +1 -4
  8. package/dist/src/lib/wizard-session.d.ts +6 -0
  9. package/dist/src/lib/wizard-session.js +1 -0
  10. package/dist/src/run.js +1 -1
  11. package/dist/src/steps/add-mcp-server-to-clients/index.js +3 -3
  12. package/dist/src/steps/add-or-update-environment-variables.js +5 -17
  13. package/dist/src/steps/run-prettier.js +1 -1
  14. package/dist/src/steps/upload-environment-variables/index.js +2 -2
  15. package/dist/src/ui/tui/components/ConsoleView.js +16 -2
  16. package/dist/src/ui/tui/console-commands.d.ts +5 -0
  17. package/dist/src/ui/tui/console-commands.js +15 -0
  18. package/dist/src/ui/tui/screens/AuthScreen.js +72 -14
  19. package/dist/src/ui/tui/screens/ChecklistScreen.js +1 -1
  20. package/dist/src/ui/tui/screens/IntroScreen.js +2 -2
  21. package/dist/src/ui/tui/screens/McpScreen.js +42 -27
  22. package/dist/src/ui/tui/screens/OutroScreen.js +1 -1
  23. package/dist/src/ui/tui/store.d.ts +17 -0
  24. package/dist/src/ui/tui/store.js +55 -19
  25. package/dist/src/utils/analytics.d.ts +27 -3
  26. package/dist/src/utils/analytics.js +83 -44
  27. package/dist/src/utils/oauth.js +1 -1
  28. package/dist/src/utils/setup-utils.d.ts +11 -0
  29. package/dist/src/utils/setup-utils.js +81 -4
  30. package/dist/src/utils/shell-completions.d.ts +2 -2
  31. package/dist/src/utils/shell-completions.js +8 -1
  32. package/dist/src/utils/track-wizard-feedback.d.ts +5 -0
  33. package/dist/src/utils/track-wizard-feedback.js +25 -0
  34. package/package.json +2 -1
@@ -46,7 +46,7 @@ export const IntroScreen = ({ store }) => {
46
46
  { label: 'Cancel', value: 'cancel' },
47
47
  ], onSelect: (value) => {
48
48
  const choice = Array.isArray(value) ? value[0] : value;
49
- analytics.wizardCapture('intro action', {
49
+ analytics.wizardCapture('Intro Action', {
50
50
  action: choice,
51
51
  integration: session.integration,
52
52
  detected_framework: session.detectedFrameworkLabel,
@@ -75,7 +75,7 @@ const FrameworkPicker = ({ store, onComplete, }) => {
75
75
  }));
76
76
  return (_jsx(PickerMenu, { centered: true, columns: 2, message: "Select your framework", options: options, onSelect: (value) => {
77
77
  const integration = Array.isArray(value) ? value[0] : value;
78
- analytics.wizardCapture('framework manually selected', { integration });
78
+ analytics.wizardCapture('Framework Manually Selected', { integration });
79
79
  void import('../../../lib/registry.js').then(({ FRAMEWORK_REGISTRY }) => {
80
80
  const config = FRAMEWORK_REGISTRY[integration];
81
81
  store.setFrameworkConfig(integration, config);
@@ -17,7 +17,7 @@ import { useSyncExternalStore } from 'react';
17
17
  import { McpOutcome, RunPhase } from '../store.js';
18
18
  import { ConfirmationInput, PickerMenu } from '../primitives/index.js';
19
19
  import { Colors } from '../styles.js';
20
- import { analytics } from '../../../utils/analytics.js';
20
+ import { analytics, captureWizardError } from '../../../utils/analytics.js';
21
21
  var Phase;
22
22
  (function (Phase) {
23
23
  Phase["Detecting"] = "detecting";
@@ -41,22 +41,25 @@ const markDone = (store, outcome, clients = [], standalone = false, onComplete)
41
41
  export const McpScreen = ({ store, installer, mode = 'install', standalone = false, onComplete, }) => {
42
42
  useSyncExternalStore((cb) => store.subscribe(cb), () => store.getSnapshot());
43
43
  const isRemove = mode === 'remove';
44
- const { runPhase, amplitudePreDetected } = store.session;
44
+ const { runPhase, amplitudePreDetected, amplitudePreDetectedChoicePending } = store.session;
45
45
  const dataSetupComplete = runPhase === RunPhase.Completed;
46
46
  const [phase, setPhase] = useState(Phase.Detecting);
47
47
  const [clients, setClients] = useState([]);
48
48
  const [resultClients, setResultClients] = useState([]);
49
49
  useEffect(() => {
50
+ if (amplitudePreDetectedChoicePending) {
51
+ return;
52
+ }
50
53
  void (async () => {
51
54
  try {
52
55
  const detected = await installer.detectClients();
53
56
  if (detected.length === 0) {
54
- analytics.wizardCapture('mcp no clients detected', { mode });
57
+ analytics.wizardCapture('MCP No Clients Detected', { mode });
55
58
  setPhase(Phase.None);
56
59
  setTimeout(() => markDone(store, McpOutcome.NoClients, [], standalone, onComplete), 1500);
57
60
  }
58
61
  else {
59
- analytics.wizardCapture('mcp clients detected', {
62
+ analytics.wizardCapture('MCP Clients Detected', {
60
63
  mode,
61
64
  clients: detected.map((c) => c.name),
62
65
  count: detected.length,
@@ -66,31 +69,31 @@ export const McpScreen = ({ store, installer, mode = 'install', standalone = fal
66
69
  }
67
70
  }
68
71
  catch {
69
- analytics.wizardCapture('mcp detection failed', { mode });
72
+ captureWizardError('MCP Client Detection', 'Editor client detection failed', 'McpScreen', { mode });
70
73
  setPhase(Phase.None);
71
74
  setTimeout(() => markDone(store, McpOutcome.Failed, [], standalone, onComplete), 1500);
72
75
  }
73
76
  })();
74
- }, [installer]);
77
+ }, [installer, amplitudePreDetectedChoicePending]);
75
78
  const handleConfirm = () => {
76
79
  if (isRemove) {
77
- analytics.wizardCapture('mcp remove confirmed');
80
+ analytics.wizardCapture('MCP Remove Confirmed');
78
81
  void doRemove();
79
82
  }
80
83
  else if (clients.length === 1) {
81
84
  const names = clients.map((c) => c.name);
82
- analytics.wizardCapture('mcp install confirmed', { clients: names });
85
+ analytics.wizardCapture('MCP Install Confirmed', { clients: names });
83
86
  void doInstall(names);
84
87
  }
85
88
  else {
86
- analytics.wizardCapture('mcp client picker shown', {
89
+ analytics.wizardCapture('MCP Client Picker Shown', {
87
90
  available_clients: clients.map((c) => c.name),
88
91
  });
89
92
  setPhase(Phase.Pick);
90
93
  }
91
94
  };
92
95
  const handleSkip = () => {
93
- analytics.wizardCapture('mcp skipped', { mode });
96
+ analytics.wizardCapture('MCP Skipped', { mode });
94
97
  markDone(store, McpOutcome.Skipped, [], standalone, onComplete);
95
98
  };
96
99
  const doInstall = async (names) => {
@@ -104,7 +107,7 @@ export const McpScreen = ({ store, installer, mode = 'install', standalone = fal
104
107
  setResultClients([]);
105
108
  }
106
109
  const failed = names.filter((n) => !result.includes(n));
107
- analytics.wizardCapture('mcp install complete', {
110
+ analytics.wizardCapture('MCP Install Complete', {
108
111
  installed: result,
109
112
  failed,
110
113
  attempted: names,
@@ -123,26 +126,38 @@ export const McpScreen = ({ store, installer, mode = 'install', standalone = fal
123
126
  catch {
124
127
  setResultClients([]);
125
128
  }
126
- analytics.wizardCapture('mcp remove complete', { removed: result });
129
+ analytics.wizardCapture('MCP Remove Complete', { removed: result });
127
130
  setPhase(Phase.Done);
128
131
  const outcome = result.length > 0 ? McpOutcome.Installed : McpOutcome.Failed;
129
132
  setTimeout(() => markDone(store, outcome, result, standalone, onComplete), 2000);
130
133
  };
131
134
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [dataSetupComplete && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "green", bold: true, children: amplitudePreDetected
132
135
  ? '\u2714 Amplitude is already configured in this project!'
133
- : '\u2714 Data setup complete!' }) })), _jsxs(Text, { bold: true, color: Colors.accent, children: ["MCP Server ", isRemove ? 'Removal' : 'Setup'] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase === Phase.Detecting && (_jsx(Text, { color: Colors.muted, children: "Detecting supported editors..." })), phase === Phase.None && (_jsxs(Text, { color: Colors.muted, children: ["No ", isRemove ? 'installed' : 'supported', " MCP clients detected. Skipping..."] })), phase === Phase.Ask && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: Colors.muted, children: ["Detected: ", clients.map((c) => c.name).join(', ')] }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmationInput, { message: isRemove
134
- ? 'Remove the Amplitude MCP server from your editor?'
135
- : 'Install the Amplitude MCP server to your editor?', confirmLabel: isRemove ? 'Remove MCP' : 'Install MCP', cancelLabel: "No thanks", onConfirm: handleConfirm, onCancel: handleSkip }) })] })), phase === Phase.Pick && (_jsx(PickerMenu, { message: "Select editor to install MCP server", options: clients.map((c) => ({
136
- label: c.name,
137
- value: c.name,
138
- })), mode: "multi", onSelect: (selected) => {
139
- const names = Array.isArray(selected) ? selected : [selected];
140
- if (names.length === 0)
141
- return;
142
- analytics.wizardCapture('mcp clients selected', {
143
- selected_clients: names,
144
- available_clients: clients.map((c) => c.name),
145
- });
146
- void doInstall(names);
147
- } })), phase === Phase.Working && (_jsxs(Text, { color: Colors.muted, children: [isRemove ? 'Removing' : 'Installing', " MCP server..."] })), phase === Phase.Done && (_jsx(Box, { flexDirection: "column", children: resultClients.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "green", bold: true, children: ['\u2714', " MCP server", ' ', isRemove ? 'removed from' : 'installed for', ":"] }), resultClients.map((name, i) => (_jsxs(Text, { children: [' ', '\u2022', " ", name] }, i)))] })) : (_jsxs(Text, { color: Colors.muted, children: [isRemove ? 'Removal' : 'Installation', " skipped."] })) }))] })] }));
136
+ : '\u2714 Data setup complete!' }) })), amplitudePreDetectedChoicePending && !isRemove && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsx(Text, { color: Colors.muted, children: "The installer skipped the automated setup step because Amplitude is already present. You can continue to editor MCP setup, or run the full setup wizard if you want to review or change the integration." }), _jsx(Box, { marginTop: 1, children: _jsx(PickerMenu, { message: "How would you like to proceed?", options: [
137
+ { label: 'Continue to MCP setup', value: 'continue' },
138
+ {
139
+ label: 'Run setup wizard anyway',
140
+ value: 'wizard',
141
+ },
142
+ ], onSelect: (value) => {
143
+ const runWizard = value === 'wizard';
144
+ analytics.wizardCapture('Amplitude Pre-Detected Choice', {
145
+ run_wizard_anyway: runWizard,
146
+ });
147
+ store.resolvePreDetectedChoice(runWizard);
148
+ } }) })] })), !amplitudePreDetectedChoicePending && (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: Colors.accent, children: ["MCP Server ", isRemove ? 'Removal' : 'Setup'] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase === Phase.Detecting && (_jsx(Text, { color: Colors.muted, children: "Detecting supported editors..." })), phase === Phase.None && (_jsxs(Text, { color: Colors.muted, children: ["No ", isRemove ? 'installed' : 'supported', " MCP clients detected. Skipping..."] })), phase === Phase.Ask && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: Colors.muted, children: ["Detected: ", clients.map((c) => c.name).join(', ')] }), _jsx(Box, { marginTop: 1, children: _jsx(ConfirmationInput, { message: isRemove
149
+ ? 'Remove the Amplitude MCP server from your editor?'
150
+ : 'Install the Amplitude MCP server to your editor?', confirmLabel: isRemove ? 'Remove MCP' : 'Install MCP', cancelLabel: "No thanks", onConfirm: handleConfirm, onCancel: handleSkip }) })] })), phase === Phase.Pick && (_jsx(PickerMenu, { message: "Select editor to install MCP server", options: clients.map((c) => ({
151
+ label: c.name,
152
+ value: c.name,
153
+ })), mode: "multi", onSelect: (selected) => {
154
+ const names = Array.isArray(selected) ? selected : [selected];
155
+ if (names.length === 0)
156
+ return;
157
+ analytics.wizardCapture('MCP Clients Selected', {
158
+ selected_clients: names,
159
+ available_clients: clients.map((c) => c.name),
160
+ });
161
+ void doInstall(names);
162
+ } })), phase === Phase.Working && (_jsxs(Text, { color: Colors.muted, children: [isRemove ? 'Removing' : 'Installing', " MCP server..."] })), phase === Phase.Done && (_jsx(Box, { flexDirection: "column", children: resultClients.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: "green", bold: true, children: ['\u2714', " MCP server", ' ', isRemove ? 'removed from' : 'installed for', ":"] }), resultClients.map((name, i) => (_jsxs(Text, { children: [' ', '\u2022', " ", name] }, i)))] })) : (_jsxs(Text, { color: Colors.muted, children: [isRemove ? 'Removal' : 'Installation', " skipped."] })) }))] })] }))] }));
148
163
  };
@@ -47,7 +47,7 @@ export const OutroScreen = ({ store }) => {
47
47
  { label: 'Exit', value: 'exit' },
48
48
  ], onSelect: (value) => {
49
49
  const choice = Array.isArray(value) ? value[0] : value;
50
- analytics.wizardCapture('outro action', {
50
+ analytics.wizardCapture('Outro Action', {
51
51
  action: choice,
52
52
  outro_kind: outroData.kind,
53
53
  });
@@ -72,6 +72,8 @@ export declare class WizardStore {
72
72
  private _backupAndFixSettings;
73
73
  /** Pending confirmation or choice prompt from the agent. */
74
74
  private $pendingPrompt;
75
+ /** Resolves when the user picks continue vs run wizard after Amplitude pre-detection. */
76
+ private _preDetectedChoiceResolver;
75
77
  constructor(flow?: Flow);
76
78
  get session(): WizardSession;
77
79
  set session(value: WizardSession);
@@ -94,6 +96,7 @@ export declare class WizardStore {
94
96
  completeSetup(): void;
95
97
  setRunPhase(phase: RunPhase): void;
96
98
  setCredentials(credentials: WizardSession['credentials']): void;
99
+ setApiKeyNotice(notice: string | null): void;
97
100
  setFrameworkConfig(integration: WizardSession['integration'], config: WizardSession['frameworkConfig']): void;
98
101
  setDetectionComplete(): void;
99
102
  setDetectedFramework(label: string): void;
@@ -188,6 +191,20 @@ export declare class WizardStore {
188
191
  */
189
192
  enableFeature(feature: AdditionalFeature): void;
190
193
  setAmplitudePreDetected(): void;
194
+ /**
195
+ * Blocks bin.ts until McpScreen resolves via resolvePreDetectedChoice().
196
+ */
197
+ waitForPreDetectedChoice(): Promise<boolean>;
198
+ /**
199
+ * Called from McpScreen when the user chooses to skip the agent (continue to
200
+ * MCP) or run the full setup wizard anyway.
201
+ */
202
+ resolvePreDetectedChoice(runWizardAnyway: boolean): void;
203
+ /**
204
+ * Undo the pre-detection fast-path so runWizard can run; used when the user
205
+ * opts into the setup agent after Amplitude was already found in the project.
206
+ */
207
+ resetForAgentAfterPreDetected(): void;
191
208
  setMcpComplete(outcome?: McpOutcome, installedClients?: string[]): void;
192
209
  setSlackComplete(outcome?: SlackOutcome): void;
193
210
  setOutroData(data: OutroData): void;
@@ -13,7 +13,7 @@ import { atom, map } from 'nanostores';
13
13
  import { TaskStatus } from '../wizard-ui.js';
14
14
  import { AdditionalFeature, McpOutcome, SlackOutcome, RunPhase, buildSession, } from '../../lib/wizard-session.js';
15
15
  import { WizardRouter, Screen, Overlay, Flow, } from './router.js';
16
- import { analytics, sessionProperties } from '../../utils/analytics.js';
16
+ import { analytics, sessionPropertiesCompact } from '../../utils/analytics.js';
17
17
  export { TaskStatus, Screen, Overlay, Flow, RunPhase, McpOutcome, SlackOutcome, };
18
18
  export class WizardStore {
19
19
  // ── Internal nanostore atoms ─────────────────────────────────────
@@ -51,6 +51,8 @@ export class WizardStore {
51
51
  _backupAndFixSettings = null;
52
52
  /** Pending confirmation or choice prompt from the agent. */
53
53
  $pendingPrompt = atom(null);
54
+ /** Resolves when the user picks continue vs run wizard after Amplitude pre-detection. */
55
+ _preDetectedChoiceResolver = null;
54
56
  constructor(flow = Flow.Wizard) {
55
57
  this.router = new WizardRouter(flow);
56
58
  }
@@ -103,7 +105,7 @@ export class WizardStore {
103
105
  */
104
106
  completeSetup() {
105
107
  this.$session.setKey('setupConfirmed', true);
106
- analytics.wizardCapture('setup confirmed', sessionProperties(this.session));
108
+ analytics.wizardCapture('Setup Confirmed', sessionPropertiesCompact(this.session));
107
109
  this._resolveSetup();
108
110
  this.emitChange();
109
111
  }
@@ -116,15 +118,16 @@ export class WizardStore {
116
118
  if (credentials?.projectId) {
117
119
  analytics.setDistinctId(String(credentials.projectId));
118
120
  }
119
- analytics.wizardCapture('auth complete', {
120
- project_id: credentials?.projectId,
121
- });
122
- analytics.wizardCapture('credentials updated', {
121
+ analytics.wizardCapture('Auth Complete', {
123
122
  project_id: credentials?.projectId,
124
123
  region: this.session.region,
125
124
  });
126
125
  this.emitChange();
127
126
  }
127
+ setApiKeyNotice(notice) {
128
+ this.$session.setKey('apiKeyNotice', notice);
129
+ this.emitChange();
130
+ }
128
131
  setFrameworkConfig(integration, config) {
129
132
  this.$session.setKey('integration', integration);
130
133
  this.$session.setKey('frameworkConfig', config);
@@ -194,7 +197,7 @@ export class WizardStore {
194
197
  const prompt = this.$pendingPrompt.get();
195
198
  if (!prompt || prompt.kind === 'event-plan')
196
199
  return;
197
- analytics.wizardCapture('prompt response', {
200
+ analytics.wizardCapture('Prompt Response', {
198
201
  prompt_kind: prompt.kind,
199
202
  response: String(answer),
200
203
  });
@@ -219,7 +222,7 @@ export class WizardStore {
219
222
  const prompt = this.$pendingPrompt.get();
220
223
  if (!prompt || prompt.kind !== 'event-plan')
221
224
  return;
222
- analytics.wizardCapture('prompt response', {
225
+ analytics.wizardCapture('Prompt Response', {
223
226
  prompt_kind: 'event-plan',
224
227
  response: typeof decision === 'object' ? 'feedback' : String(decision),
225
228
  });
@@ -303,7 +306,7 @@ export class WizardStore {
303
306
  }
304
307
  setDataIngestionConfirmed() {
305
308
  this.$session.setKey('dataIngestionConfirmed', true);
306
- analytics.wizardCapture('data ingestion confirmed', sessionProperties(this.session));
309
+ analytics.wizardCapture('Data Ingestion Confirmed', sessionPropertiesCompact(this.session));
307
310
  this.emitChange();
308
311
  }
309
312
  setChecklistChartComplete() {
@@ -316,10 +319,10 @@ export class WizardStore {
316
319
  }
317
320
  setChecklistComplete() {
318
321
  this.$session.setKey('checklistComplete', true);
319
- analytics.wizardCapture('checklist completed', {
322
+ analytics.wizardCapture('Checklist Completed', {
320
323
  chart_complete: this.session.checklistChartComplete,
321
324
  dashboard_complete: this.session.checklistDashboardComplete,
322
- ...sessionProperties(this.session),
325
+ ...sessionPropertiesCompact(this.session),
323
326
  });
324
327
  this.emitChange();
325
328
  }
@@ -411,30 +414,62 @@ export class WizardStore {
411
414
  if (feature === AdditionalFeature.LLM) {
412
415
  this.session.llmOptIn = true;
413
416
  }
414
- analytics.wizardCapture('feature enabled', { feature });
417
+ analytics.wizardCapture('Feature Enabled', { feature });
415
418
  this.emitChange();
416
419
  }
417
420
  setAmplitudePreDetected() {
418
421
  this.$session.setKey('amplitudePreDetected', true);
422
+ this.$session.setKey('amplitudePreDetectedChoicePending', true);
423
+ this.emitChange();
424
+ }
425
+ /**
426
+ * Blocks bin.ts until McpScreen resolves via resolvePreDetectedChoice().
427
+ */
428
+ waitForPreDetectedChoice() {
429
+ return new Promise((resolve) => {
430
+ this._preDetectedChoiceResolver = resolve;
431
+ });
432
+ }
433
+ /**
434
+ * Called from McpScreen when the user chooses to skip the agent (continue to
435
+ * MCP) or run the full setup wizard anyway.
436
+ */
437
+ resolvePreDetectedChoice(runWizardAnyway) {
438
+ const resolveFn = this._preDetectedChoiceResolver;
439
+ this._preDetectedChoiceResolver = null;
440
+ if (!runWizardAnyway) {
441
+ this.$session.setKey('amplitudePreDetectedChoicePending', false);
442
+ }
443
+ this.emitChange();
444
+ resolveFn?.(runWizardAnyway);
445
+ }
446
+ /**
447
+ * Undo the pre-detection fast-path so runWizard can run; used when the user
448
+ * opts into the setup agent after Amplitude was already found in the project.
449
+ */
450
+ resetForAgentAfterPreDetected() {
451
+ this.$session.setKey('amplitudePreDetected', false);
452
+ this.$session.setKey('amplitudePreDetectedChoicePending', false);
453
+ this.$session.setKey('runPhase', RunPhase.Idle);
419
454
  this.emitChange();
420
455
  }
421
456
  setMcpComplete(outcome = McpOutcome.Skipped, installedClients = []) {
422
457
  this.$session.setKey('mcpComplete', true);
423
458
  this.$session.setKey('mcpOutcome', outcome);
424
459
  this.$session.setKey('mcpInstalledClients', installedClients);
425
- analytics.wizardCapture('mcp complete', {
460
+ analytics.wizardCapture('MCP Complete', {
426
461
  mcp_outcome: outcome,
427
462
  mcp_installed_clients: installedClients,
428
- ...sessionProperties(this.session),
463
+ ...sessionPropertiesCompact(this.session),
429
464
  });
430
465
  this.emitChange();
431
466
  }
432
467
  setSlackComplete(outcome = SlackOutcome.Skipped) {
433
468
  this.$session.setKey('slackComplete', true);
434
469
  this.$session.setKey('slackOutcome', outcome);
435
- analytics.wizardCapture('slack complete', {
470
+ analytics.wizardCapture('Slack Complete', {
436
471
  slack_outcome: outcome,
437
- ...sessionProperties(this.session),
472
+ ...sessionPropertiesCompact(this.session),
438
473
  });
439
474
  this.emitChange();
440
475
  }
@@ -512,9 +547,10 @@ export class WizardStore {
512
547
  for (const fn of hooks)
513
548
  fn();
514
549
  }
515
- analytics.wizardCapture(`screen ${next}`, {
516
- from_screen: prev,
517
- ...sessionProperties(this.session),
550
+ analytics.wizardCapture('Wizard Screen Entered', {
551
+ screen_name: next,
552
+ previous_screen: prev,
553
+ ...sessionPropertiesCompact(this.session),
518
554
  });
519
555
  }
520
556
  }
@@ -1,17 +1,29 @@
1
1
  import type { WizardSession } from '../lib/wizard-session';
2
+ /**
3
+ * Telemetry project API key. Empty or whitespace-only env value means “no key”
4
+ * (use default only when the variable is unset).
5
+ */
6
+ export declare function resolveTelemetryApiKey(): string;
7
+ /** HTTP API URL for `@amplitude/analytics-node` (same shape as manual HTTP ingest). */
8
+ export declare function getAmplitudeNodeServerUrl(): string;
2
9
  /**
3
10
  * Extract a standard property bag from the current session.
4
11
  * Used by store-level analytics and available for ad-hoc captures.
5
12
  */
6
13
  export declare function sessionProperties(session: WizardSession): Record<string, unknown>;
14
+ /**
15
+ * Smaller session bag for high-volume wizard events (taxonomy: keep event
16
+ * properties chart-useful and within a small count).
17
+ */
18
+ export declare function sessionPropertiesCompact(session: WizardSession): Record<string, unknown>;
7
19
  export declare class Analytics {
8
20
  private tags;
9
21
  private distinctId?;
10
22
  private anonymousId;
11
23
  private appName;
12
24
  private activeFlags;
13
- private pendingEvents;
14
- private flushTimer;
25
+ private readonly client;
26
+ private initPromise;
15
27
  constructor();
16
28
  setDistinctId(distinctId: string): void;
17
29
  setTag(key: string, value: string | boolean | number | null | undefined): void;
@@ -20,9 +32,11 @@ export declare class Analytics {
20
32
  /**
21
33
  * Capture a wizard-specific event. Automatically prepends "wizard: " to the event name.
22
34
  * All new wizard analytics should use this method instead of capture() directly.
35
+ * Use lowercase with spaces for eventName (e.g. "agent started", "api key submitted")
36
+ * per Amplitude quickstart taxonomy guidelines.
23
37
  */
24
38
  wizardCapture(eventName: string, properties?: Record<string, unknown>): void;
25
- private flush;
39
+ private ensureInitStarted;
26
40
  getFeatureFlag(flagKey: string): Promise<string | boolean | undefined>;
27
41
  /**
28
42
  * Evaluate all feature flags for the current user at the start of a run.
@@ -32,4 +46,14 @@ export declare class Analytics {
32
46
  getAllFlagsForWizard(): Promise<Record<string, string>>;
33
47
  shutdown(status: 'success' | 'error' | 'cancelled'): Promise<void>;
34
48
  }
49
+ /**
50
+ * Full Amplitude `event_type` for CLI/TUI product feedback.
51
+ * Same string as `wizardCapture('Feedback Submitted', …)`.
52
+ */
53
+ export declare const WIZARD_FEEDBACK_EVENT_TYPE = "wizard: feedback submitted";
35
54
  export declare const analytics: Analytics;
55
+ /**
56
+ * Unified wizard error telemetry (aligns with starter taxonomy “Error Encountered”).
57
+ * Emits `wizard: error encountered` with category / message / context.
58
+ */
59
+ export declare function captureWizardError(errorCategory: string, errorMessage: string, errorContext: string, extra?: Record<string, unknown>): void;
@@ -1,11 +1,29 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.analytics = exports.Analytics = void 0;
3
+ exports.analytics = exports.WIZARD_FEEDBACK_EVENT_TYPE = exports.Analytics = void 0;
4
+ exports.resolveTelemetryApiKey = resolveTelemetryApiKey;
5
+ exports.getAmplitudeNodeServerUrl = getAmplitudeNodeServerUrl;
4
6
  exports.sessionProperties = sessionProperties;
7
+ exports.sessionPropertiesCompact = sessionPropertiesCompact;
8
+ exports.captureWizardError = captureWizardError;
9
+ const analytics_node_1 = require("@amplitude/analytics-node");
5
10
  const uuid_1 = require("uuid");
6
11
  const debug_1 = require("./debug");
7
- const AMPLITUDE_API_KEY = process.env.AMPLITUDE_API_KEY ?? 'e5a2c9bdffe949f7da77e6b481e118fa';
8
- const AMPLITUDE_SERVER_URL = process.env.AMPLITUDE_SERVER_URL ?? 'https://api2.amplitude.com';
12
+ const DEFAULT_TELEMETRY_API_KEY = 'e5a2c9bdffe949f7da77e6b481e118fa';
13
+ /**
14
+ * Telemetry project API key. Empty or whitespace-only env value means “no key”
15
+ * (use default only when the variable is unset).
16
+ */
17
+ function resolveTelemetryApiKey() {
18
+ const fromEnv = process.env.AMPLITUDE_API_KEY;
19
+ const raw = fromEnv !== undefined ? fromEnv : DEFAULT_TELEMETRY_API_KEY;
20
+ return raw.trim();
21
+ }
22
+ /** HTTP API URL for `@amplitude/analytics-node` (same shape as manual HTTP ingest). */
23
+ function getAmplitudeNodeServerUrl() {
24
+ const base = process.env.AMPLITUDE_SERVER_URL ?? 'https://api2.amplitude.com';
25
+ return `${base.replace(/\/$/, '')}/2/httpapi`;
26
+ }
9
27
  /**
10
28
  * Extract a standard property bag from the current session.
11
29
  * Used by store-level analytics and available for ad-hoc captures.
@@ -21,18 +39,31 @@ function sessionProperties(session) {
21
39
  run_phase: session.runPhase,
22
40
  };
23
41
  }
42
+ /**
43
+ * Smaller session bag for high-volume wizard events (taxonomy: keep event
44
+ * properties chart-useful and within a small count).
45
+ */
46
+ function sessionPropertiesCompact(session) {
47
+ return {
48
+ integration: session.integration,
49
+ detected_framework: session.detectedFrameworkLabel,
50
+ run_phase: session.runPhase,
51
+ project_id: session.credentials?.projectId,
52
+ };
53
+ }
24
54
  class Analytics {
25
55
  tags = {};
26
56
  distinctId;
27
57
  anonymousId;
28
58
  appName = 'wizard';
29
59
  activeFlags = null;
30
- pendingEvents = [];
31
- flushTimer = null;
60
+ client;
61
+ initPromise = null;
32
62
  constructor() {
33
63
  this.tags = { $app_name: this.appName };
34
64
  this.anonymousId = (0, uuid_1.v4)();
35
65
  this.distinctId = undefined;
66
+ this.client = (0, analytics_node_1.createInstance)();
36
67
  }
37
68
  setDistinctId(distinctId) {
38
69
  this.distinctId = distinctId;
@@ -48,59 +79,45 @@ class Analytics {
48
79
  });
49
80
  }
50
81
  capture(eventName, properties) {
51
- if (!AMPLITUDE_API_KEY) {
82
+ const apiKey = resolveTelemetryApiKey();
83
+ if (!apiKey) {
52
84
  (0, debug_1.debug)('capture (no API key):', eventName, properties);
53
85
  return;
54
86
  }
55
- const event = {
56
- event_type: eventName,
87
+ this.ensureInitStarted();
88
+ const eventProps = {
89
+ ...this.tags,
90
+ ...properties,
91
+ };
92
+ const options = {
57
93
  device_id: this.anonymousId,
58
- time: Date.now(),
59
- event_properties: {
60
- ...this.tags,
61
- ...properties,
62
- },
63
94
  };
64
95
  if (this.distinctId) {
65
- event.user_id = this.distinctId;
96
+ options.user_id = this.distinctId;
66
97
  }
67
- this.pendingEvents.push(event);
98
+ this.client.track(eventName, eventProps, options);
68
99
  (0, debug_1.debug)('capture:', eventName, properties);
69
- // Debounce flush to batch events
70
- if (this.flushTimer) {
71
- clearTimeout(this.flushTimer);
72
- }
73
- this.flushTimer = setTimeout(() => {
74
- void this.flush();
75
- }, 500);
76
100
  }
77
101
  /**
78
102
  * Capture a wizard-specific event. Automatically prepends "wizard: " to the event name.
79
103
  * All new wizard analytics should use this method instead of capture() directly.
104
+ * Use lowercase with spaces for eventName (e.g. "agent started", "api key submitted")
105
+ * per Amplitude quickstart taxonomy guidelines.
80
106
  */
81
107
  wizardCapture(eventName, properties) {
82
108
  this.capture(`wizard: ${eventName}`, properties);
83
109
  }
84
- async flush() {
85
- if (this.pendingEvents.length === 0)
86
- return;
87
- if (!AMPLITUDE_API_KEY)
110
+ ensureInitStarted() {
111
+ if (this.initPromise !== null) {
88
112
  return;
89
- const events = [...this.pendingEvents];
90
- this.pendingEvents = [];
91
- try {
92
- const response = await fetch(`${AMPLITUDE_SERVER_URL}/2/httpapi`, {
93
- method: 'POST',
94
- headers: { 'Content-Type': 'application/json' },
95
- body: JSON.stringify({ api_key: AMPLITUDE_API_KEY, events }),
96
- });
97
- if (!response.ok) {
98
- (0, debug_1.debug)('Amplitude upload failed:', response.status, await response.text());
99
- }
100
113
  }
101
- catch (err) {
102
- (0, debug_1.debug)('Amplitude upload error:', err);
114
+ const apiKey = resolveTelemetryApiKey();
115
+ if (!apiKey) {
116
+ return;
103
117
  }
118
+ this.initPromise = this.client.init(apiKey, {
119
+ serverUrl: getAmplitudeNodeServerUrl(),
120
+ }).promise;
104
121
  }
105
122
  // eslint-disable-next-line @typescript-eslint/require-await
106
123
  async getFeatureFlag(flagKey) {
@@ -121,13 +138,35 @@ class Analytics {
121
138
  return this.activeFlags;
122
139
  }
123
140
  async shutdown(status) {
124
- if (this.flushTimer) {
125
- clearTimeout(this.flushTimer);
126
- this.flushTimer = null;
141
+ this.wizardCapture('Session Ended', { status });
142
+ if (this.initPromise === null) {
143
+ return;
144
+ }
145
+ try {
146
+ await this.initPromise;
147
+ await this.client.flush().promise;
148
+ }
149
+ catch (err) {
150
+ (0, debug_1.debug)('analytics shutdown flush error:', err);
127
151
  }
128
- this.capture('wizard: session ended', { status });
129
- await this.flush();
130
152
  }
131
153
  }
132
154
  exports.Analytics = Analytics;
155
+ /**
156
+ * Full Amplitude `event_type` for CLI/TUI product feedback.
157
+ * Same string as `wizardCapture('Feedback Submitted', …)`.
158
+ */
159
+ exports.WIZARD_FEEDBACK_EVENT_TYPE = 'wizard: feedback submitted';
133
160
  exports.analytics = new Analytics();
161
+ /**
162
+ * Unified wizard error telemetry (aligns with starter taxonomy “Error Encountered”).
163
+ * Emits `wizard: error encountered` with category / message / context.
164
+ */
165
+ function captureWizardError(errorCategory, errorMessage, errorContext, extra) {
166
+ exports.analytics.wizardCapture('Error Encountered', {
167
+ error_category: errorCategory,
168
+ error_message: errorMessage,
169
+ error_context: errorContext,
170
+ ...extra,
171
+ });
172
+ }
@@ -315,7 +315,7 @@ async function performAmplitudeAuth(options) {
315
315
  (0, index_js_1.getUI)().log.info(`${chalk_1.default.yellow('Authorization was cancelled.')}\n\nRe-run the wizard to try again.`);
316
316
  }
317
317
  else {
318
- (0, index_js_1.getUI)().log.error(`${chalk_1.default.red('Authorization failed:')}\n\n${error.message}\n\n${chalk_1.default.dim(`File an issue:\n${constants_js_1.ISSUES_URL}`)}`);
318
+ (0, index_js_1.getUI)().log.error(`${chalk_1.default.red('Authorization failed:')}\n\n${error.message}\n\n${chalk_1.default.dim(`File an issue:\n${constants_js_1.OUTBOUND_URLS.githubIssues}`)}`);
319
319
  }
320
320
  analytics_js_1.analytics.captureException(error, { step: 'oauth_flow' });
321
321
  await (0, setup_utils_js_1.abort)();