Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2

- name: Use Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # 4.4.0
with:
node-version: '18.19.0'
node-version: '22.13.0'

- name: Cache Node.js modules
uses: actions/cache@v2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: ~/.npm
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ yarn-error.log
dist
coderefs/
.vscode/settings.json
.idea
.idea
.yarn/
package-lock.json
Comment thread
charlottemorgan marked this conversation as resolved.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ The LaunchDarkly VSCode extension provides utilities that make it easy to work w

<img src="https://github.com/launchdarkly/ld-vscode/raw/master/images/screenshot.png?raw=true" alt="screenshot" width="100%">

## Authentication

The LaunchDarkly VS Code extension requires authentication using a **Personal Access Token** with **Writer** permissions or higher.

#### Important Notes:
- **Personal tokens only**: Service tokens are not supported
- **Permissions**: Your token must have permissions to perform actions available in the extension. Reader roles will be able to view flags but not perform any actions.
- **Token scope**: The token should have access to the projects and environments you want to work with

#### [Creating an Access Token](https://launchdarkly.com/docs/home/account/api-create#create-access-tokens)


#### Sign in:

1. Open VS Code with the LaunchDarkly extension installed
2. Use the Command Palette (`Cmd/Ctrl + Shift + P`)
3. Run `LaunchDarkly: Sign In`
4. Select your LaunchDarkly instance (Commercial, Federal, or Other)
5. Paste your Personal Access Token when prompted


## Features

- Feature flag details tooltip on hover
Expand Down Expand Up @@ -91,3 +112,41 @@ This is OFF by default. It can be enabled through Settings > LaunchDarkly Extens
## Contributing

LaunchDarkly for Visual Studio Code is an [open source project](https://github.com/launchdarkly/ld-vscode). If you experience any issues, please [log an issue on our issue tracker](https://github.com/launchdarkly/ld-vscode/issues). If you'd like to contribute, we're happily taking pull requests.

## Development

### Prerequisites
- Node.js (version 18 or higher)
- [Visual Studio Code](https://code.visualstudio.com/)

### Getting Started

**Install dependencies**
```bash
npm install
```

**Build the extension**
```bash
npm run compile
```

**Start debugging**
- Press `F5` or go to `Run > Start Debugging`
- This will open a new VS Code window with the extension loaded in development mode
- The extension will be available as "LaunchDarkly" in the new window

### Development Workflow

- **Watch mode**: Run `npm run watch` to automatically recompile on file changes
- **Testing**: Run `npm test` to execute the test suite
- **Linting**: Run `npm run lint` to check code style

### Debugging

- Set breakpoints in the TypeScript source files
- Use the Debug Console in the host VS Code window to inspect variables
- Check the Developer Tools (`Help > Toggle Developer Tools`) in the extension development host for console output



21 changes: 13 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,8 @@
"title": "LaunchDarkly: Quick Targeting"
},
{
"command": "vscode-launchdarkly-authprovider.signIn",
"title": "Sign in",
"category": "LaunchDarkly",
"icon": "$(sign-in)",
"when": false
"command": "launchdarkly.signIn",
"title": "LaunchDarkly: Sign in"
},
{
"command": "launchdarkly.setMaintainer",
Expand All @@ -255,6 +252,10 @@
{
"command": "launchdarkly.quickFlag",
"title": "LaunchDarkly: Flag Actions"
},
{
"command": "launchdarkly.signOut",
"title": "LaunchDarkly: Sign Out"
}
],
"menus": {
Expand Down Expand Up @@ -344,6 +345,11 @@
"command": "launchdarkly.configureLaunchDarklyEnvironment",
"when": "view == launchdarklyFeatureFlags",
"group": "navigation"
},
{
"command": "launchdarkly.signOut",
"when": "view == launchdarklyFeatureFlags && launchdarkly:isSignedIn",
"group": "inline"
}
],
"view/item/context": [
Expand Down Expand Up @@ -461,7 +467,7 @@
},
{
"view": "launchdarklyFeatureFlags",
"contents": "In order to manage LaunchDarkly feature flags, you will need to configure this extension first.\n[Configure](command:extension.configureLaunchDarkly)\n[Read our docs](https://docs.launchdarkly.com/integrations/vscode) to learn more about how to use the LaunchDarkly extension. \nIf you had a previous version installed you may need to \n[Migrate Configuration](command:launchdarkly.migrateConfiguration)\n Previous versions allowed for global configuration, the current version is configured per workspace.",
"contents": "In order to manage LaunchDarkly feature flags, you will need to sign in.\n Once signed in you can configure your project and environment. \n [Sign in](command:launchdarkly.signIn)\n[Read our docs](https://docs.launchdarkly.com/integrations/vscode) to learn more about how to use the LaunchDarkly extension.",
"when": "config.launchdarkly.globalDefault == 'off'"
}
]
Expand Down Expand Up @@ -526,7 +532,7 @@
"@types/lodash": "4.14.116",
"@types/opn": "5.1.0",
"ajv": "^6.12.6",
"axios": "0.21.4",
"axios": "^1.6.0",
"axios-retry": "4.0.0",
"axios-retry-after": "2.0.0",
"csv-parser": "^2.3.3",
Expand All @@ -541,7 +547,6 @@
"request": "^2.34",
"rimraf": "^3.0.2",
"tar-fs": "^2.1.1",
"tree-sitter-stack-graphs": "^0.7.0",
"uuid": "^9.0.1"
},
"resolutions": {
Expand Down
40 changes: 17 additions & 23 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as url from 'url';
import { authentication, commands, window } from 'vscode';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios').default;
import axios from 'axios';
import axiosRetry from 'axios-retry';
import retry from 'axios-retry-after';

Expand All @@ -22,7 +21,7 @@ import { LDExtensionConfiguration } from './ldExtensionConfiguration';
import { debuglog } from 'util';
import { CMD_LD_CONFIG } from './utils/commands';
import { legacyAuth } from './utils/legacyAuth';
import { CONST_CONFIG_LD } from './utils/constants';
import { CONST_CONFIG_LD, CONST_LD_PREFIX } from './utils/constants';

interface CreateOptionsParams {
method?: string;
Expand All @@ -40,11 +39,11 @@ axios.interceptors.response.use(
},
async function (error) {
const originalRequest = error.config;
if (error.response.status === 404) {
if (error.response?.status === 404) {
debuglog(error);
debuglog(`404 for URL: ${originalRequest.url}`);
}
if (error.response.status === 401 && !originalRequest._retry) {
if (error.response?.status === 401 && !originalRequest._retry) {
const config = LDExtensionConfiguration.getInstance();
originalRequest._retry = true;
const session = (await authentication.getSession('launchdarkly', ['writer'], {
Expand All @@ -64,15 +63,15 @@ axios.interceptors.response.use(
isRetryable(error) {
return (
error.response &&
error.response.status === 429 &&
error.response.headers['X-Ratelimit-Reset'] &&
error.response.headers['X-Ratelimit-Reset'] <= 60
error.response?.status === 429 &&
error.response?.headers['X-Ratelimit-Reset'] &&
error.response?.headers['X-Ratelimit-Reset'] <= 60
);
},

// Customize the wait behavior
wait(error) {
return new Promise((resolve) => setTimeout(resolve, error.response.headers['X-Ratelimit-Reset']));
return new Promise((resolve) => setTimeout(resolve, error.response?.headers['X-Ratelimit-Reset']));
},

// Customize the retry request itself
Expand Down Expand Up @@ -111,7 +110,7 @@ export class LaunchDarklyAPI {

try {
//data = await this.executeWithRetry(() => axios.get(options.url, { ...options }), 2);
data = await axios.get(options.url, { ...options });
data = await axios.get(options.url, options);
} catch (err) {
console.log(err);
return [];
Expand Down Expand Up @@ -143,7 +142,7 @@ export class LaunchDarklyAPI {
const initialUrl = `projects/${projectKey}?expand=environments`;
const requestUrl = url || initialUrl;
const options = this.createOptions(requestUrl, { method: 'GET' });
const data = await axios.get(options.url, { ...options });
const data = await axios.get(options.url, options);
const project = data.data;

if (project.environments._links && project.environments._links.next) {
Expand Down Expand Up @@ -197,7 +196,7 @@ export class LaunchDarklyAPI {
console.log(err);
window
.showErrorMessage(
`${CONST_CONFIG_LD} Error getting Project: ${projectKey} Environment: ${envKey}\n${err}`,
`${CONST_LD_PREFIX} Error getting Project: ${projectKey} Environment: ${envKey}\n${err}`,
CONST_CONFIG_LD,
)
.then((selection) => {
Expand Down Expand Up @@ -288,29 +287,24 @@ export class LaunchDarklyAPI {
}
const envParam = envKey ? 'env=' + envKey : '';
const limit = 100;
const initialUrl = `flags/${projectKey}/?${envParam}&summary=true&sort=name&limit=${limit}`;
const initialUrl = `flags/${projectKey}?${envParam}&summary=true&sort=name&limit=${limit}`;
const requestUrl = url || initialUrl;
const options = this.createOptions(requestUrl, { method: 'GET', params: envParam });
let data;

try {
//data = await this.executeWithRetry(() => axios.get(options.url, { ...options }), 2);
data = await axios.get(options.url, { ...options });
data = await axios.get(options.url, { headers: { ...options.headers } });
} catch (err) {
console.log(err);
console.log('err in getFeatureFlags---->>>', err);
return [];
}
const flags = data.data.items;
if (data.data._links && data.data._links.next) {
// If there is a 'next' link, fetch the next page
if (data?.data?._links && data?.data?._links.next) {
const match = '/api/v2/';
const nextLink = data.data._links.next.href.replace(new RegExp(match), '');
//await sleep(1500);
//const moreFlags = await this.executeWithRetry(async () => await this.getFeatureFlags(projectKey, envKey, nextLink), 2);
const nextLink = data?.data?._links.next.href.replace(new RegExp(match), '');
const moreFlags = await this.getFeatureFlags(projectKey, envKey, nextLink);
return flags.concat(moreFlags);
} else {
// If there is no 'next' link, all items have been fetched
return flags;
}
}
Expand All @@ -322,7 +316,7 @@ export class LaunchDarklyAPI {
body: value,
});
const data = await axios.patch(options.url, value, options);
return new FeatureFlag(data);
return new FeatureFlag(data.data);
} catch (err) {
return Promise.reject(err);
}
Expand Down
5 changes: 3 additions & 2 deletions src/commands/evaluateFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ export async function flagEval(config: LDExtensionConfiguration, key) {
`Flag Details:\n${JSON.stringify(flagValue.value)}\n
Reason:\n${JSON.stringify(flagValue.reason.kind)}\n
Rule:\n${JSON.stringify(rule.description ? rule.description : rule._id)}
[Open in Browser](${config.getSession()?.fullUri}/${config.getConfig()?.project}/${config.getConfig()
?.env}/features/${key}/targeting#${rule._id})`,
[Open in Browser](${config.getSession()?.fullUri}/${config.getConfig()?.project}/${
config.getConfig()?.env
}/features/${key}/targeting#${rule._id})`,
{ modal: true },
{ title: 'Open in Browser', command: 'launchdarkly.openRule', arguments: [key, rule._id] },
);
Expand Down
69 changes: 55 additions & 14 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { extensionReload } from './generalUtils';
import { LDExtensionConfiguration } from './ldExtensionConfiguration';
import * as semver from 'semver';
import { SetWorkspaceCmd } from './commands/setWorkspaceEnabled';
import { CMD_LD_CONFIG, CMD_LD_SIGNIN } from './utils/commands';
import { CMD_LD_CONFIG, CMD_LD_SIGNIN, CMD_LD_SIGNOUT } from './utils/commands';
import { ILaunchDarklyAuthenticationSession } from './models';

export async function activate(ctx: ExtensionContext): Promise<void> {
Expand All @@ -27,11 +27,13 @@ export async function activate(ctx: ExtensionContext): Promise<void> {
const session = (await authentication.getSession('launchdarkly', ['writer'], {
createIfNone: false,
})) as ILaunchDarklyAuthenticationSession;
LDExtConfig.setSession(session);

const validationError = await LDExtConfig.getConfig().validate();
const configuredOnce = LDExtConfig.getCtx().globalState.get('LDConfigured');

LDExtConfig.setSession(session);
commands.executeCommand('setContext', 'launchdarkly:isSignedIn', !!session);

switch (validationError) {
case 'unconfigured':
if (window.activeTextEditor !== undefined && configuredOnce !== true) {
Expand Down Expand Up @@ -65,22 +67,61 @@ export async function activate(ctx: ExtensionContext): Promise<void> {

LDExtConfig.getCtx().subscriptions.push(
commands.registerCommand(CMD_LD_SIGNIN, async () => {
const session = (await authentication.getSession('launchdarkly', ['writer'], {
createIfNone: true,
})) as ILaunchDarklyAuthenticationSession;
LDExtConfig.setSession(session);
if (!(await LDExtConfig.getConfig().isConfigured())) {
window
.showInformationMessage(`Click Configure below to finish setting up the LaunchDarkly extension`, `Configure`)
.then((item) => {
item === 'Configure' ? commands.executeCommand(CMD_LD_CONFIG) : null;
});
} else {
window.showInformationMessage(`You are now signed in to LaunchDarkly & Project is configured.`);
try {
const session = (await authentication.getSession('launchdarkly', ['writer'], {
createIfNone: true,
})) as ILaunchDarklyAuthenticationSession;

if (!session || !session.account) return;

LDExtConfig.setSession(session);
await LDExtConfig.getConfig().reload();

if (!(await LDExtConfig.getConfig().isConfigured())) {
window
.showInformationMessage(
`Click Configure below to finish setting up the LaunchDarkly extension`,
`Configure`,
)
.then((item) => {
item === 'Configure' ? commands.executeCommand(CMD_LD_CONFIG) : null;
});
} else {
commands.executeCommand('setContext', 'launchdarkly:isSignedIn', true);
window.showInformationMessage(`You are now signed in to LaunchDarkly & Project is configured.`);
}
} catch (err) {
console.error('Error in sign in command:', err);
commands.executeCommand('setContext', 'launchdarkly:isSignedIn', false);
const selection = await window.showErrorMessage(`Sign in error: ${err.message}`, { modal: true }, 'Try Again');

if (selection === 'Try Again') {
commands.executeCommand(CMD_LD_SIGNIN);
}
}
}),
SetWorkspaceCmd(LDExtConfig),
);

LDExtConfig.getCtx().subscriptions.push(
commands.registerCommand(CMD_LD_SIGNOUT, async () => {
const confirmSignOut = await window.showWarningMessage(
`Are you sure you want to sign out of LaunchDarkly?`,
{ modal: true },
'Sign Out',
);

if (confirmSignOut === 'Sign Out') {
await authProv.removeSession(LDExtConfig.getSession()?.id);
LDExtConfig.setSession(null);
commands.executeCommand('setContext', 'launchdarkly:isSignedIn', false);
window.showInformationMessage(`You are now signed out of LaunchDarkly.`);
}

await extensionReload(LDExtConfig);
}),
);

authentication.onDidChangeSessions(async (e) => {
if (e.provider.id === 'launchdarkly') {
await extensionReload(LDExtConfig);
Expand Down
Loading