Skip to content

Commit 3e0cc08

Browse files
committed
feat: add internal analytics client
1 parent 23fa8fc commit 3e0cc08

10 files changed

Lines changed: 688 additions & 306 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ coderefs/
88
.vscode/settings.json
99
.idea
1010
.yarn/
11-
package-lock.json
11+
package-lock.json
12+
.env*

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@
540540
"@vscode/test-electron": "^2.3.8",
541541
"chai": "^4.3.10",
542542
"clean-webpack-plugin": "4.0.0",
543+
"dotenv": "^17.3.1",
543544
"eslint": "8.55.0",
544545
"eslint-config-prettier": "9.1.0",
545546
"expect": "^26.1.0",
@@ -572,6 +573,7 @@
572573
"form-data": "^4.0.0",
573574
"gunzip-maybe": "^1.4.2",
574575
"launchdarkly-api-typescript": "6.0.0",
576+
"launchdarkly-node-client-sdk": "^3.3.1",
575577
"lodash": "^4.17.23",
576578
"lodash.debounce": "4.0.8",
577579
"lodash.kebabcase": "4.1.1",

src/analytics.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as LDClient from 'launchdarkly-node-client-sdk';
2+
import { Disposable, env } from 'vscode';
3+
4+
const CLIENT_SIDE_ID = process.env.LD_ANALYTICS_CLIENT_ID;
5+
const BASE_URL = process.env.LD_ANALYTICS_BASE_URL;
6+
const STREAM_URL = process.env.LD_ANALYTICS_STREAM_URL;
7+
const EVENTS_URL = process.env.LD_ANALYTICS_EVENTS_URL;
8+
9+
/**
10+
* Internal analytics client using the LaunchDarkly Node Client SDK.
11+
* Uses a client-side ID (safe to embed — not a secret) to send
12+
* usage telemetry events. Respects VS Code telemetry settings.
13+
*/
14+
class AnalyticsClient implements Disposable {
15+
private client: LDClient.LDClient | null = null;
16+
private enabled = false;
17+
private initPromise: Promise<void> | null = null;
18+
19+
async initialize(extensionVersion: string): Promise<void> {
20+
console.log('env.isTelemetryEnabled', env.isTelemetryEnabled);
21+
console.log('LD_ANALYTICS_CLIENT_ID', CLIENT_SIDE_ID);
22+
console.log('LD_ANALYTICS_BASE_URL', BASE_URL);
23+
console.log('LD_ANALYTICS_STREAM_URL', STREAM_URL);
24+
console.log('LD_ANALYTICS_EVENTS_URL', EVENTS_URL);
25+
26+
if (!CLIENT_SIDE_ID || !BASE_URL || !STREAM_URL || !EVENTS_URL || !env.isTelemetryEnabled) {
27+
return;
28+
}
29+
30+
if (this.initPromise) {
31+
return this.initPromise;
32+
}
33+
34+
this.initPromise = this.doInitialize(extensionVersion);
35+
return this.initPromise;
36+
}
37+
38+
private async doInitialize(extensionVersion: string): Promise<void> {
39+
try {
40+
const context: LDClient.LDContext = {
41+
kind: 'ld-vscode-user',
42+
key: env.machineId,
43+
vscodeVersion: env.appName,
44+
extensionVersion,
45+
};
46+
47+
this.client = LDClient.initialize(CLIENT_SIDE_ID, context, {
48+
baseUrl: BASE_URL,
49+
streamUrl: STREAM_URL,
50+
eventsUrl: EVENTS_URL,
51+
});
52+
await this.client.waitForInitialization();
53+
this.enabled = true;
54+
55+
env.onDidChangeTelemetryEnabled((telemetryEnabled) => {
56+
this.enabled = telemetryEnabled;
57+
});
58+
} catch (err) {
59+
console.error('[LaunchDarkly Analytics] Failed to initialize:', err);
60+
this.client = null;
61+
this.enabled = false;
62+
}
63+
}
64+
65+
track(event: string, data?: LDClient.LDFlagValue): void {
66+
if (!this.enabled || !this.client) {
67+
return;
68+
}
69+
try {
70+
this.client.track(event, data);
71+
} catch (err) {
72+
console.error(`[LaunchDarkly Analytics] Failed to track event "${event}":`, err);
73+
}
74+
}
75+
76+
async dispose(): Promise<void> {
77+
if (this.client) {
78+
try {
79+
await this.client.flush();
80+
} catch {
81+
// Best-effort flush on shutdown
82+
}
83+
this.client.close();
84+
this.client = null;
85+
}
86+
this.enabled = false;
87+
this.initPromise = null;
88+
}
89+
}
90+
91+
export const analytics = new AnalyticsClient();

src/commands/connectDevServer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { LDExtensionConfiguration } from '../ldExtensionConfiguration';
33
import { CMD_LD_CONNECT_DEV_SERVER, CMD_LD_DISCONNECT_DEV_SERVER, CMD_LD_REFRESH_ENTRY } from '../utils/commands';
44
import { registerCommand } from '../utils/registerCommand';
55
import { updateDevServerStatusBar } from '../devServerStatusBar';
6+
import { analytics } from '../analytics';
67

78
const DEFAULT_DEV_SERVER_URI = 'http://localhost:8765';
89

@@ -90,6 +91,7 @@ export function connectDevServerCommand(config: LDExtensionConfiguration): Dispo
9091
// Refresh the flags view to load dev-server values
9192
await commands.executeCommand(CMD_LD_REFRESH_ENTRY);
9293

94+
analytics.track('dev-server-connected', { uri: finalUri });
9395
window.showInformationMessage(`Connected to LaunchDarkly dev-server at ${finalUri}`);
9496
} catch (err) {
9597
console.error(`Failed to connect to dev-server: ${err}`);
@@ -129,6 +131,7 @@ export function disconnectDevServerCommand(config: LDExtensionConfiguration): Di
129131
// Refresh the flags view to show LaunchDarkly values
130132
await commands.executeCommand(CMD_LD_REFRESH_ENTRY);
131133

134+
analytics.track('dev-server-disconnected');
132135
window.showInformationMessage('Disconnected from dev-server. Flag values now come from LaunchDarkly.');
133136
} catch (err) {
134137
console.error(`Failed to disconnect from dev-server: ${err}`);

src/commands/devServerOverrides.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ILDExtensionConfiguration } from '../models';
22
import { commands, window } from 'vscode';
33
import { showSmartOverrideInput } from '../utils/smartOverrideInput';
4+
import { analytics } from '../analytics';
45

56
export async function setDevServerOverride(config: ILDExtensionConfiguration, flagKey: string): Promise<void> {
67
const devServerProvider = config.getDevServerProvider();
@@ -36,6 +37,7 @@ export async function setDevServerOverride(config: ILDExtensionConfiguration, fl
3637
const success = await devServerProvider.setOverride(flagKey, value);
3738

3839
if (success) {
40+
analytics.track('dev-server-override-set', { flagKey, isEditing });
3941
window.showInformationMessage(`${isEditing ? 'Updated' : 'Set'} dev-server override for flag "${flagKey}"`);
4042
await commands.executeCommand('launchdarkly.refreshEntry');
4143
} else {
@@ -67,6 +69,7 @@ export async function removeDevServerOverride(config: ILDExtensionConfiguration,
6769
const success = await devServerProvider.removeOverride(flagKey);
6870

6971
if (success) {
72+
analytics.track('dev-server-override-removed', { flagKey });
7073
window.showInformationMessage(`Removed dev-server override for flag "${flagKey}"`);
7174
await commands.executeCommand('launchdarkly.refreshEntry');
7275
} else {

src/commands/flagActions.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { flagCodeSearch } from '../utils/flagCodeSearch';
77
import { registerCommand } from '../utils/registerCommand';
88
import { ILDExtensionConfiguration } from '../models';
99
import { removeDevServerOverride, setDevServerOverride } from './devServerOverrides';
10+
import { analytics } from '../analytics';
1011

1112
const cache = new ToggleCache();
1213

@@ -104,12 +105,14 @@ export default function flagCmd(config: ILDExtensionConfiguration): Disposable {
104105
commands.executeCommand(CMD_LD_OPEN_BROWSER, linkUrl);
105106
break;
106107
}
107-
case 'Toggle Flag':
108-
await toggleFlag(config, flagWindow.value);
109-
break;
110-
case 'Search Flag':
111-
flagCodeSearch(config, flagWindow.value);
112-
break;
108+
case 'Toggle Flag':
109+
analytics.track('flag-toggled', { flagKey: flagWindow.value });
110+
await toggleFlag(config, flagWindow.value);
111+
break;
112+
case 'Search Flag':
113+
analytics.track('flag-search-used', { flagKey: flagWindow.value });
114+
flagCodeSearch(config, flagWindow.value);
115+
break;
113116
case 'Update fallthrough variation':
114117
flagOffFallthroughPatch(config, 'updateFallthroughVariationOrRollout', flagWindow.value);
115118
break;

src/extension.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ILaunchDarklyAuthenticationSession } from './models';
1818
import { connectDevServerCommand, disconnectDevServerCommand } from './commands/connectDevServer';
1919
import { createDevServerStatusBar, updateDevServerStatusBar } from './devServerStatusBar';
2020
import { DevServerProvider } from './providers/devServerProvider';
21+
import { analytics } from './analytics';
2122

2223
export async function activate(ctx: ExtensionContext): Promise<void> {
2324
const storedVersion = ctx.globalState.get('version', '5.0.0');
@@ -79,6 +80,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
7980

8081
LDExtConfig.setSession(session);
8182
await LDExtConfig.getConfig().reload();
83+
analytics.track('user-signed-in');
8284

8385
if (!(await LDExtConfig.getConfig().isConfigured())) {
8486
window
@@ -115,6 +117,7 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
115117
);
116118

117119
if (confirmSignOut === 'Sign Out') {
120+
analytics.track('user-signed-out');
118121
await authProv.removeSession(LDExtConfig.getSession()?.id);
119122
LDExtConfig.setSession(null);
120123
commands.executeCommand('setContext', 'launchdarkly:isSignedIn', false);
@@ -185,6 +188,13 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
185188
console.log(err);
186189
}
187190

191+
// Initialize analytics (non-blocking — failures are silently ignored)
192+
const extensionVersion = ctx.extension?.packageJSON?.version ?? 'unknown';
193+
ctx.subscriptions.push(analytics);
194+
analytics.initialize(extensionVersion).then(() => {
195+
analytics.track('extension-activated');
196+
});
197+
188198
// Attempt to auto-reconnect to dev-server if it was previously connected
189199
await attemptDevServerReconnect(LDExtConfig);
190200
}
@@ -266,5 +276,6 @@ async function handleDevServerConnectionFailure(config: LDExtensionConfiguration
266276
}
267277

268278
export async function deactivate(): Promise<void> {
279+
await analytics.dispose();
269280
global.ldContext.flagStore && global.ldContext.flagStore.stop();
270281
}

src/types/env.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
declare namespace NodeJS {
2+
interface ProcessEnv {
3+
LD_ANALYTICS_CLIENT_ID?: string;
4+
}
5+
}

webpack.config.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Heavily inspired and copied from Gitlens, special thanks to @eamodio for amazing community support.
66
'use strict';
77

8+
require('dotenv').config();
89
const path = require('path');
910
const { spawnSync } = require('child_process');
1011
const { DefinePlugin } = require('webpack');
@@ -13,7 +14,13 @@ const { validate } = require('schema-utils');
1314

1415
function getExtensionConfig(mode) {
1516
const plugins = [
16-
new DefinePlugin({ 'global.GENTLY': false }),
17+
new DefinePlugin({
18+
'global.GENTLY': false,
19+
'process.env.LD_ANALYTICS_CLIENT_ID': JSON.stringify(process.env.LD_ANALYTICS_CLIENT_ID || ''),
20+
'process.env.LD_ANALYTICS_BASE_URL': JSON.stringify(process.env.LD_ANALYTICS_BASE_URL || ''),
21+
'process.env.LD_ANALYTICS_STREAM_URL': JSON.stringify(process.env.LD_ANALYTICS_STREAM_URL || ''),
22+
'process.env.LD_ANALYTICS_EVENTS_URL': JSON.stringify(process.env.LD_ANALYTICS_EVENTS_URL || ''),
23+
}),
1724
new FantasticonPlugin({
1825
configPath: '.fantasticonrc.js',
1926
onBefore:

0 commit comments

Comments
 (0)