diff --git a/packages/toolbar/package.json b/packages/toolbar/package.json
index 90082c51..e478b764 100644
--- a/packages/toolbar/package.json
+++ b/packages/toolbar/package.json
@@ -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",
@@ -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"
diff --git a/packages/toolbar/rslib.config.ts b/packages/toolbar/rslib.config.ts
index 4608a97e..dfab8f28 100644
--- a/packages/toolbar/rslib.config.ts
+++ b/packages/toolbar/rslib.config.ts
@@ -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()],
diff --git a/packages/toolbar/src/core/tests/FlagSdkOverrideProvider.test.tsx b/packages/toolbar/src/core/tests/FlagSdkOverrideProvider.test.tsx
index ef9a21dc..39ab7e95 100644
--- a/packages/toolbar/src/core/tests/FlagSdkOverrideProvider.test.tsx
+++ b/packages/toolbar/src/core/tests/FlagSdkOverrideProvider.test.tsx
@@ -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(
+
+
+ ,
+ );
+
+ 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);
diff --git a/packages/toolbar/src/core/ui/Toolbar/components/new/Contexts/AddContextForm.tsx b/packages/toolbar/src/core/ui/Toolbar/components/new/Contexts/AddContextForm.tsx
index 8733df75..33b85ddf 100644
--- a/packages/toolbar/src/core/ui/Toolbar/components/new/Contexts/AddContextForm.tsx
+++ b/packages/toolbar/src/core/ui/Toolbar/components/new/Contexts/AddContextForm.tsx
@@ -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';
diff --git a/packages/toolbar/src/core/ui/Toolbar/components/new/Contexts/ContextItem.tsx b/packages/toolbar/src/core/ui/Toolbar/components/new/Contexts/ContextItem.tsx
index 719c7ca2..2ccd9640 100644
--- a/packages/toolbar/src/core/ui/Toolbar/components/new/Contexts/ContextItem.tsx
+++ b/packages/toolbar/src/core/ui/Toolbar/components/new/Contexts/ContextItem.tsx
@@ -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';
diff --git a/packages/toolbar/src/core/ui/Toolbar/context/FlagSdkOverrideProvider.tsx b/packages/toolbar/src/core/ui/Toolbar/context/FlagSdkOverrideProvider.tsx
index 9c218682..523614fb 100644
--- a/packages/toolbar/src/core/ui/Toolbar/context/FlagSdkOverrideProvider.tsx
+++ b/packages/toolbar/src/core/ui/Toolbar/context/FlagSdkOverrideProvider.tsx
@@ -107,16 +107,28 @@ export function FlagSdkOverrideProvider({ children, flagOverridePlugin }: FlagSd
setIsLoading(false);
// Subscribe to changes with incremental updates
- const handleChange = (changes: Record) => {
+ // 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, 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;
diff --git a/packages/toolbar/src/core/ui/Toolbar/context/api/ContextsProvider.tsx b/packages/toolbar/src/core/ui/Toolbar/context/api/ContextsProvider.tsx
index dc935b8f..c1e84659 100644
--- a/packages/toolbar/src/core/ui/Toolbar/context/api/ContextsProvider.tsx
+++ b/packages/toolbar/src/core/ui/Toolbar/context/api/ContextsProvider.tsx
@@ -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';
diff --git a/packages/toolbar/src/core/ui/Toolbar/utils/localStorage.ts b/packages/toolbar/src/core/ui/Toolbar/utils/localStorage.ts
index 4fd9b24b..cb89eccf 100644
--- a/packages/toolbar/src/core/ui/Toolbar/utils/localStorage.ts
+++ b/packages/toolbar/src/core/ui/Toolbar/utils/localStorage.ts
@@ -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 = {
diff --git a/packages/toolbar/src/core/utils/analytics.ts b/packages/toolbar/src/core/utils/analytics.ts
index e83afd06..b6d9774b 100644
--- a/packages/toolbar/src/core/utils/analytics.ts
+++ b/packages/toolbar/src/core/utils/analytics.ts
@@ -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';
diff --git a/packages/toolbar/src/core/utils/feedback.ts b/packages/toolbar/src/core/utils/feedback.ts
index 3c9e750b..3615a580 100644
--- a/packages/toolbar/src/core/utils/feedback.ts
+++ b/packages/toolbar/src/core/utils/feedback.ts
@@ -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';
diff --git a/packages/toolbar/src/types/plugins/LDClient.ts b/packages/toolbar/src/types/plugins/LDClient.ts
new file mode 100644
index 00000000..6a288d8b
--- /dev/null
+++ b/packages/toolbar/src/types/plugins/LDClient.ts
@@ -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 | Promise | Promise;
+ addHook(hook: Hook): void;
+ getHooks?(metadata: LDPluginEnvironmentMetadata): Hook[];
+ allFlags(): LDFlagSet;
+ getContext(): any;
+ on(key: string, callback: (...args: any[]) => void): void;
+ off(key: string, callback: (...args: any[]) => void): void;
+ flush(): void;
+}
diff --git a/packages/toolbar/src/types/plugins/eventInterceptionPlugin.ts b/packages/toolbar/src/types/plugins/eventInterceptionPlugin.ts
index 47d2218e..6a5fe929 100644
--- a/packages/toolbar/src/types/plugins/eventInterceptionPlugin.ts
+++ b/packages/toolbar/src/types/plugins/eventInterceptionPlugin.ts
@@ -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
diff --git a/packages/toolbar/src/types/plugins/flagOverridePlugin.ts b/packages/toolbar/src/types/plugins/flagOverridePlugin.ts
index 548326f0..5fe5ee3b 100644
--- a/packages/toolbar/src/types/plugins/flagOverridePlugin.ts
+++ b/packages/toolbar/src/types/plugins/flagOverridePlugin.ts
@@ -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
diff --git a/packages/toolbar/src/types/plugins/plugins.ts b/packages/toolbar/src/types/plugins/plugins.ts
index 592d6517..1460ce2d 100644
--- a/packages/toolbar/src/types/plugins/plugins.ts
+++ b/packages/toolbar/src/types/plugins/plugins.ts
@@ -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, LDDebugOverride {
+ register(client: LDClient, metadata: LDPluginEnvironmentMetadata): void;
+
/**
* Sets an override value for a feature flag
* @param flagKey - The key of the flag to override
@@ -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 {
+ register(client: LDClient, metadata: LDPluginEnvironmentMetadata): void;
/**
* Gets all intercepted events from the event store
* @returns Array of processed events
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 56831f39..bd236bf6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -150,6 +150,9 @@ importers:
'@codemirror/view':
specifier: ^6.39.11
version: 6.39.11
+ '@launchdarkly/js-client-sdk':
+ specifier: ^4.0.0
+ version: 4.0.0
'@launchdarkly/observability':
specifier: ^1.0.0
version: 1.0.0
@@ -1186,9 +1189,15 @@ packages:
'@launchdarkly/js-client-sdk-common@1.16.0':
resolution: {integrity: sha512-W6yVVlIV/BqCLR1je3uReVfJcTNK9mznp1PWpEEBRRrapjMnLYUcCJat6yj3dyyn3VKyawXy1MkSn34gEEicjg==}
+ '@launchdarkly/js-client-sdk-common@1.17.2':
+ resolution: {integrity: sha512-MV4I8h2a/C6UKxsoPMLQW0ileb/qj30gfCyQl2KsM2ZJGP3hwN2mkKUUwytlL+RURpN+0XGpuQ6UIQFfePUTgQ==}
+
'@launchdarkly/js-client-sdk@0.11.0':
resolution: {integrity: sha512-PRmSsPsOi3NXjXpG4D4MoyImBibNoh9wOoWFbIMXHA26h5cMNBQjTFI6gqe8kyf2rDwqHerhrj5f734Zqwl33Q==}
+ '@launchdarkly/js-client-sdk@4.0.0':
+ resolution: {integrity: sha512-jTKbVmfDsx/PsvtFsTpZTjZxp6HP5zQSMnz1okC3z/foVBsC4FxMrlWjMgZnYMNeoe2MOjDHOo6s9eQxI+zCew==}
+
'@launchdarkly/js-sdk-common@2.20.0':
resolution: {integrity: sha512-g1Lyi5xL7AXAP6BP8BzRcqVqIhqOSVpA5Bx8Vvj8A/4A6sIHVz2vIZluykD/bJiKg1+G9ojm+OCfdL/c0ebi0A==}
@@ -6017,10 +6026,18 @@ snapshots:
dependencies:
'@launchdarkly/js-sdk-common': 2.20.0
+ '@launchdarkly/js-client-sdk-common@1.17.2':
+ dependencies:
+ '@launchdarkly/js-sdk-common': 2.20.0
+
'@launchdarkly/js-client-sdk@0.11.0':
dependencies:
'@launchdarkly/js-client-sdk-common': 1.16.0
+ '@launchdarkly/js-client-sdk@4.0.0':
+ dependencies:
+ '@launchdarkly/js-client-sdk-common': 1.17.2
+
'@launchdarkly/js-sdk-common@2.20.0': {}
'@launchdarkly/observability@1.0.0':