Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/toolbar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
"@codemirror/lint": "^6.9.3",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.11",
"@launchdarkly/js-client-sdk": "^4.0.0",
"@launchdarkly/observability": "^1.0.0",
"@launchdarkly/session-replay": "^1.0.0",
"@launchpad-ui/components": "^0.17.12",
Expand Down Expand Up @@ -186,6 +187,7 @@
"@angular/common": ">=14.0.0 <22.0.0",
"@angular/core": ">=14.0.0 <22.0.0",
"launchdarkly-js-client-sdk": ">=3.9.0 <4.0.0",
"@launchdarkly/js-client-sdk": "^0.11.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"vue": "^2.7.0 || ^3.0.0"
Expand Down
1 change: 1 addition & 0 deletions packages/toolbar/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default defineConfig({
'@angular/common': '@angular/common',
rxjs: 'rxjs',
'launchdarkly-js-client-sdk': 'launchdarkly-js-client-sdk',
'@launchdarkly/js-client-sdk': '@launchdarkly/js-client-sdk',
},
},
plugins: [pluginReact()],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,41 @@ describe('FlagSdkOverrideProvider', () => {
expect(screen.getByTestId('flag-dynamic-flag')).toHaveTextContent('Dynamic Flag: updated (original)');
});

test('handles LaunchDarkly client change events from client v4', async () => {
mockLdClient.allFlags.mockReturnValue({
'dynamic-flag': 'initial',
});

let changeHandler: (context: object, changedKeys: string[]) => void;
mockLdClient.on.mockImplementation((event: string, handler: any) => {
if (event === 'change') {
changeHandler = handler;
}
});

render(
<FlagSdkOverrideProvider flagOverridePlugin={mockFlagOverridePlugin}>
<TestConsumer />
</FlagSdkOverrideProvider>,
);

await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});

expect(screen.getByTestId('flag-dynamic-flag')).toHaveTextContent('Dynamic Flag: initial (original)');

mockLdClient.allFlags.mockReturnValue({
'dynamic-flag': 'updated',
});

await act(async () => {
changeHandler!({}, ['dynamic-flag']);
});

expect(screen.getByTestId('flag-dynamic-flag')).toHaveTextContent('Dynamic Flag: updated (original)');
});

test('handles null LaunchDarkly client gracefully', async () => {
// GIVEN: Plugin returns null client (edge case)
(mockFlagOverridePlugin.getClient as any).mockReturnValue(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useCallback, useMemo } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import type { LDContext } from 'launchdarkly-js-client-sdk';
import type { LDContext } from '@launchdarkly/js-client-sdk';
import { useContextsContext } from '../../../context/api/ContextsProvider';
import { CancelIcon } from '../../icons';
import { EASING } from '../../../constants';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useCallback, useMemo, useEffect, useRef, memo } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import type { LDContext } from 'launchdarkly-js-client-sdk';
import type { LDContext } from '@launchdarkly/js-client-sdk';
import * as styles from './ContextItem.module.css';
import { CopyableText } from '../../CopyableText';
import { EditIcon, DeleteIcon, CheckIcon, CancelIcon } from '../../icons';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,28 @@ export function FlagSdkOverrideProvider({ children, flagOverridePlugin }: FlagSd
setIsLoading(false);

// Subscribe to changes with incremental updates
const handleChange = (changes: Record<string, { current: any }>) => {
// NOTE: a better way to do this might be to be able to use the LDPluginEnvironmentMetadata that
// is passed in when the plugin is registered. That property has the client version number which can
// then be used to determine how to adapt the change handler.
// Currently the change handler is set up to be able to handle both <= v3 and >= v4 change events.
// <= v3: changes are passed as the first argument and as a map of flag keys and their changed values.
// >= v4: changes are passed as the second argument (the first argument is the context) and is an array of flag keys
// of changed flags.
const handleChange = (changes: Record<string, { current: any }>, keys?: string[]) => {
setFlags((prevFlags) => {
const updatedRawFlags = ldClient.allFlags();
const newFlags = buildFlags(updatedRawFlags, apiFlags);

let changedKeys = keys;
if (changedKeys === undefined) {
changedKeys = Object.keys(changes);
}

// Only update the flags that actually changed for better performance
const updatedFlags = { ...prevFlags };
let hasChanges = false;

Object.keys(changes).forEach((flagKey) => {
changedKeys.forEach((flagKey) => {
if (newFlags[flagKey]) {
updatedFlags[flagKey] = newFlags[flagKey];
hasChanges = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import type { LDContext } from 'launchdarkly-js-client-sdk';
import type { LDContext } from '@launchdarkly/js-client-sdk';
import { loadContexts, saveContexts, loadActiveContext, saveActiveContext } from '../../utils/localStorage';
import { usePlugins } from '../state/PluginsProvider';
import { getContextDisplayName, getContextKey, getContextKind, getStableContextId } from '../../utils/context';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { LDContext } from 'launchdarkly-js-client-sdk';
import type { LDContext } from '@launchdarkly/js-client-sdk';
import { ToolbarPosition, TOOLBAR_POSITIONS } from '../types/toolbar';

export const TOOLBAR_STORAGE_KEYS = {
Expand Down
3 changes: 1 addition & 2 deletions packages/toolbar/src/core/utils/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { LDClient } from 'launchdarkly-js-client-sdk';

import type { LDClient } from '../../types/plugins/LDClient';
import type { FeedbackSentiment } from '../../types/analytics';
import { isDoNotTrackEnabled } from './browser';
import { sendFeedback } from './feedback';
Expand Down
2 changes: 1 addition & 1 deletion packages/toolbar/src/core/utils/feedback.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LDRecord } from '@launchdarkly/session-replay';
import type { LDClient } from 'launchdarkly-js-client-sdk';
import type { LDClient } from '../../types/plugins/LDClient';

export type LDFeedbackSentiment = 'positive' | 'neutral' | 'negative';

Expand Down
19 changes: 19 additions & 0 deletions packages/toolbar/src/types/plugins/LDClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LDFlagSet, LDPluginEnvironmentMetadata } from 'launchdarkly-js-client-sdk';
import { LDIdentifyResult, Hook } from '@launchdarkly/js-client-sdk';

/**
* LDClient based on the LDClient type in the SDK package with
* a narrower structure to ensure that they can be used by
* our plugins.
*/
export interface LDClient {
track(key: string, data?: any, metricValue?: number): void;
identify(ctx: any): Promise<LDIdentifyResult> | Promise<LDFlagSet> | Promise<void>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the LDContext type here.

Suggested change
identify(ctx: any): Promise<LDIdentifyResult> | Promise<LDFlagSet> | Promise<void>;
identify(ctx: LDContext): Promise<LDIdentifyResult> | Promise<LDFlagSet> | Promise<void>;

Copy link
Copy Markdown
Author

@joker23 joker23 Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with using LDContext directly here is that it conflicts with the type definition in launchdarkly-react-client-sdk which is very old at this point... structurally, it is hard to compat launchdarkly-react-client-sdk, launchdarkly-js-client-sdk and @launchdarkly/react-client-sdk which is why I put the compat client to accept any... we can probably still maintain type safety if we narrow the context when it is being used?

We are looking at updating the React SDK soon so it should be more compatible in the future.

I don't think this suggestion will build.

addHook(hook: Hook): void;
getHooks?(metadata: LDPluginEnvironmentMetadata): Hook[];
allFlags(): LDFlagSet;
getContext(): any;
Comment thread
pranjal-jately-ld marked this conversation as resolved.
on(key: string, callback: (...args: any[]) => void): void;
off(key: string, callback: (...args: any[]) => void): void;
flush(): void;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Hook, LDClient, LDPluginEnvironmentMetadata, LDPluginMetadata } from 'launchdarkly-js-client-sdk';
import type { LDPluginEnvironmentMetadata, LDPluginMetadata, Hook } from '@launchdarkly/js-client-sdk';
import { AfterTrackHook, AfterIdentifyHook, AfterEvaluationHook, EventStore } from '../hooks';
import type { EventFilter, ProcessedEvent } from '../events';
import type { IEventInterceptionPlugin } from './plugins';
import { ANALYTICS_EVENT_PREFIX } from '../analytics';
import type { LDClient } from './LDClient';

/**
* Configuration options for the EventInterceptionPlugin
Expand Down
4 changes: 2 additions & 2 deletions packages/toolbar/src/types/plugins/flagOverridePlugin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type {
LDClient,
LDDebugOverride,
LDPluginMetadata,
LDFlagSet,
Hook,
LDPluginEnvironmentMetadata,
} from 'launchdarkly-js-client-sdk';
} from '@launchdarkly/js-client-sdk';
import type { IFlagOverridePlugin } from './plugins';
import type { LDClient } from './LDClient';

/**
* Configuration options for the FlagOverridePlugin
Expand Down
16 changes: 13 additions & 3 deletions packages/toolbar/src/types/plugins/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import type { LDClient, LDDebugOverride, LDFlagSet, LDFlagValue, LDPlugin } from 'launchdarkly-js-client-sdk';
import type {
LDDebugOverride,
LDFlagSet,
LDFlagValue,
LDPlugin,
LDPluginEnvironmentMetadata,
} from '@launchdarkly/js-client-sdk';
import type { LDClient } from './LDClient';
import type { ProcessedEvent } from '../events';

export interface IFlagOverridePlugin extends LDPlugin, LDDebugOverride {
export interface IFlagOverridePlugin extends Omit<LDPlugin, 'register'>, LDDebugOverride {
register(client: LDClient, metadata: LDPluginEnvironmentMetadata): void;

/**
* Sets an override value for a feature flag
* @param flagKey - The key of the flag to override
Expand Down Expand Up @@ -36,7 +45,8 @@ export interface IFlagOverridePlugin extends LDPlugin, LDDebugOverride {
/**
* Interface for event interception plugins that can be used with the LaunchDarkly Toolbar
*/
export interface IEventInterceptionPlugin extends LDPlugin {
export interface IEventInterceptionPlugin extends Omit<LDPlugin, 'register'> {
register(client: LDClient, metadata: LDPluginEnvironmentMetadata): void;
/**
* Gets all intercepted events from the event store
* @returns Array of processed events
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading