diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml index acf12f4f5d..f8f18af583 100644 --- a/.github/workflows/browser.yml +++ b/.github/workflows/browser.yml @@ -102,12 +102,21 @@ jobs: echo $! > /tmp/playwright.pid sleep 5 # Give the browser time to initialize and connect via WebSocket - - name: Run contract tests + - name: Run contract tests (FDv1) uses: launchdarkly/gh-actions/actions/contract-tests@d271978e893b5b9facb9f000414e9fcd62e1f78b with: test_service_port: 8000 token: ${{ secrets.GITHUB_TOKEN }} - extra_params: '--skip-from=${{ github.workspace }}/packages/sdk/browser/contract-tests/suppressions.txt --stop-service-at-end' + stop_service: 'false' + extra_params: '--skip-from=${{ github.workspace }}/packages/sdk/browser/contract-tests/suppressions.txt' + + - name: Run contract tests (FDv2) + uses: launchdarkly/gh-actions/actions/contract-tests@d271978e893b5b9facb9f000414e9fcd62e1f78b + with: + test_service_port: 8000 + token: ${{ secrets.GITHUB_TOKEN }} + version: v3 + extra_params: '--skip-from=${{ github.workspace }}/packages/sdk/browser/contract-tests/suppressions_datamode_changes.txt' - name: Print logs on failure if: failure() diff --git a/.github/workflows/react-native-contract-tests.yml b/.github/workflows/react-native-contract-tests.yml index 58ee2bf0ae..503124236e 100644 --- a/.github/workflows/react-native-contract-tests.yml +++ b/.github/workflows/react-native-contract-tests.yml @@ -87,16 +87,13 @@ jobs: sleep 1 done - - name: Download contract test harness - run: | - # https://github.com/launchdarkly/sdk-test-harness/releases/tag/v2.34.0 - curl -sL -o sdk-test-harness.tar.gz "https://github.com/launchdarkly/sdk-test-harness/releases/download/v2.34.0/sdk-test-harness_Linux_x86_64.tar.gz" - tar -xzf sdk-test-harness.tar.gz sdk-test-harness - chmod +x sdk-test-harness - - name: Run contract tests on Android emulator # https://github.com/ReactiveCircus/android-emulator-runner/releases/tag/v2.34.0 uses: reactivecircus/android-emulator-runner@f0d1ed2dcad93c7479e8b2f2226c83af54494915 # v2 + env: + # The contract-test runner script downloads the harness via the + # official downloader; the token avoids GitHub API rate limits. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: api-level: 31 arch: x86_64 diff --git a/packages/sdk/browser/example-fdv2/src/app.ts b/packages/sdk/browser/example-fdv2/src/app.ts index ffaa39e23d..45beaf8179 100644 --- a/packages/sdk/browser/example-fdv2/src/app.ts +++ b/packages/sdk/browser/example-fdv2/src/app.ts @@ -232,7 +232,6 @@ const main = async () => { buildUI(); const client = createClient(clientSideID, contexts[currentContextIndex], { - // @ts-ignore dataSystem is @internal — experimental FDv2 opt-in dataSystem: {}, logger: basicLogger({ level: 'debug' }), }); @@ -284,14 +283,12 @@ const main = async () => { ]; connectionModes.forEach((mode) => { document.getElementById(`btn-mode-${mode}`)!.addEventListener('click', () => { - // @ts-ignore setConnectionMode is @internal — experimental FDv2 opt-in client.setConnectionMode(mode); updateModeStatus(mode); log(`setConnectionMode('${mode}')`); }); }); document.getElementById('btn-mode-clear')!.addEventListener('click', () => { - // @ts-ignore setConnectionMode is @internal — experimental FDv2 opt-in client.setConnectionMode(undefined); updateModeStatus(undefined); log('setConnectionMode(undefined)'); diff --git a/packages/sdk/browser/src/LDClient.ts b/packages/sdk/browser/src/LDClient.ts index 5a86a4835e..3c6b097b39 100644 --- a/packages/sdk/browser/src/LDClient.ts +++ b/packages/sdk/browser/src/LDClient.ts @@ -35,12 +35,6 @@ export type LDClient = Omit { /** - * @internal - * - * This feature is experimental and should NOT be considered ready for - * production use. It may change or be removed without notice and is not - * subject to backwards compatibility guarantees. - * * Configuration for the FDv2 data system. When present, the SDK uses * the FDv2 protocol for flag delivery instead of the default FDv1 * protocol. * * The browser SDK restricts `automaticModeSwitching` to `false` or - * {@link ManualModeSwitching} only — automatic switching has no effect + * {@link ManualModeSwitching} only -- automatic switching has no effect * in browser environments. + * + * This option is not stable, and not subject to any backwards compatibility + * guarantees or semantic versioning. It is in early access. If you want access + * to this feature please join the EAP. + * https://launchdarkly.com/docs/sdk/features/data-saving-mode */ dataSystem?: BrowserDataSystemOptions; /** diff --git a/packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts b/packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts index 2ff7f5c3ac..5ce17f0429 100644 --- a/packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts +++ b/packages/sdk/react-native/contract-tests/entity/src/ClientEntity.ts @@ -3,6 +3,9 @@ import { CommandType, CreateInstanceParams, makeLogger, + SDKConfigDataInitializer, + SDKConfigDataSynchronizer, + SDKConfigModeDefinition, SDKConfigParams, ClientSideTestHook as TestHook, ValueType, @@ -16,6 +19,59 @@ import { export const badCommandError = new Error('unsupported command'); export const malformedCommand = new Error('command was malformed'); +function translateInitializer(init: SDKConfigDataInitializer): any | undefined { + if (init.polling) { + return { + type: 'polling', + ...(init.polling.pollIntervalMs !== undefined && { + pollInterval: init.polling.pollIntervalMs / 1000, + }), + ...(init.polling.baseUri && { + endpoints: { pollingBaseUri: init.polling.baseUri }, + }), + }; + } + return undefined; +} + +function translateSynchronizer(sync: SDKConfigDataSynchronizer): any | undefined { + if (sync.streaming) { + return { + type: 'streaming', + ...(sync.streaming.initialRetryDelayMs !== undefined && { + initialReconnectDelay: sync.streaming.initialRetryDelayMs / 1000, + }), + ...(sync.streaming.baseUri && { + endpoints: { streamingBaseUri: sync.streaming.baseUri }, + }), + }; + } + if (sync.polling) { + return { + type: 'polling', + ...(sync.polling.pollIntervalMs !== undefined && { + pollInterval: sync.polling.pollIntervalMs / 1000, + }), + ...(sync.polling.baseUri && { + endpoints: { pollingBaseUri: sync.polling.baseUri }, + }), + }; + } + return undefined; +} + +function translateModeDefinition(modeDef: SDKConfigModeDefinition): any { + const initializers = (modeDef.initializers ?? []) + .map(translateInitializer) + .filter((x) => x !== undefined); + + const synchronizers = (modeDef.synchronizers ?? []) + .map(translateSynchronizer) + .filter((x) => x !== undefined); + + return { initializers, synchronizers }; +} + function makeSdkConfig(options: SDKConfigParams, tag: string) { if (!options.clientSide) { throw new Error('configuration did not include clientSide options'); @@ -39,21 +95,80 @@ function makeSdkConfig(options: SDKConfigParams, tag: string) { cf.eventsUri = options.serviceEndpoints.events; } - if (options.polling) { - if (options.polling.baseUri) { - cf.baseUri = options.polling.baseUri; - } - cf.initialConnectionMode = 'polling'; + if (options.dataSystem?.payloadFilter) { + cf.payloadFilterKey = options.dataSystem.payloadFilter; } - // Can contain streaming and polling, if streaming is set override the initial connection - // mode. - if (options.streaming) { - if (options.streaming.baseUri) { - cf.streamUri = options.streaming.baseUri; + if (options.dataSystem) { + const dataSystem: any = {}; + + // Helper to apply endpoint overrides from a mode definition to global URIs. + const applyEndpointOverrides = (modeDef: SDKConfigModeDefinition) => { + (modeDef.synchronizers ?? []).forEach((sync) => { + if (sync.streaming?.baseUri) { + cf.streamUri = sync.streaming.baseUri; + cf.streamInitialReconnectDelay = maybeTime(sync.streaming.initialRetryDelayMs); + } + if (sync.polling?.baseUri) { + cf.baseUri = sync.polling.baseUri; + } + }); + (modeDef.initializers ?? []).forEach((init) => { + if (init.polling?.baseUri) { + cf.baseUri = init.polling.baseUri; + } + }); + }; + + if (options.dataSystem.connectionModeConfig) { + const connMode = options.dataSystem.connectionModeConfig; + dataSystem.automaticModeSwitching = connMode.initialConnectionMode + ? { type: 'manual', initialConnectionMode: connMode.initialConnectionMode } + : false; + + if (connMode.customConnectionModes) { + const connectionModes: Record = {}; + Object.entries(connMode.customConnectionModes).forEach(([modeName, modeDef]) => { + connectionModes[modeName] = translateModeDefinition(modeDef); + applyEndpointOverrides(modeDef); + }); + dataSystem.connectionModes = connectionModes; + } + } else if (options.dataSystem.initializers || options.dataSystem.synchronizers) { + // Top-level initializers/synchronizers (no connection modes). Wrap them + // into a single 'streaming' connection mode. + const modeDef: SDKConfigModeDefinition = { + initializers: options.dataSystem.initializers, + synchronizers: options.dataSystem.synchronizers, + }; + dataSystem.automaticModeSwitching = { + type: 'manual', + initialConnectionMode: 'streaming', + }; + dataSystem.connectionModes = { + streaming: translateModeDefinition(modeDef), + }; + applyEndpointOverrides(modeDef); + } + + cf.dataSystem = dataSystem; + } else { + if (options.polling) { + if (options.polling.baseUri) { + cf.baseUri = options.polling.baseUri; + } + cf.initialConnectionMode = 'polling'; + } + + // Can contain streaming and polling, if streaming is set override the initial connection + // mode. + if (options.streaming) { + if (options.streaming.baseUri) { + cf.streamUri = options.streaming.baseUri; + } + cf.initialConnectionMode = 'streaming'; + cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs); } - cf.initialConnectionMode = 'streaming'; - cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs); } if (options.events) { diff --git a/packages/sdk/react-native/contract-tests/run-ci-contract-tests.sh b/packages/sdk/react-native/contract-tests/run-ci-contract-tests.sh index 12d520b10f..fd15ebf286 100755 --- a/packages/sdk/react-native/contract-tests/run-ci-contract-tests.sh +++ b/packages/sdk/react-native/contract-tests/run-ci-contract-tests.sh @@ -56,14 +56,32 @@ while [ "$i" -lt 30 ]; do sleep 2 done -# Run the contract test harness -SUPPRESSIONS_FILE="$SCRIPT_DIR/suppressions.txt" -EXTRA_ARGS="" -if [ -s "$SUPPRESSIONS_FILE" ]; then - EXTRA_ARGS="--skip-from=$SUPPRESSIONS_FILE" +# Fetch the official contract-test-harness runner once. This is the same +# downloader the launchdarkly/gh-actions contract-tests action uses; VERSION +# selects the harness release (v2 -> latest v2.x for FDv1, v3 -> latest v3.x +# for FDv2), and GITHUB_TOKEN (from the workflow env) avoids API rate limits. +# This mirrors the android-client-sdk contract-test setup. +HARNESS_RUNNER=/tmp/run-test-harness.sh +curl -sf \ + https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \ + -o "$HARNESS_RUNNER" + +# FDv1 (v2 harness). +FDV1_SUPPRESSIONS="$SCRIPT_DIR/suppressions.txt" +FDV1_SKIP="" +if [ -s "$FDV1_SUPPRESSIONS" ]; then + FDV1_SKIP="--skip-from=$FDV1_SUPPRESSIONS" +fi + +echo "=== Running FDv1 contract tests ===" +VERSION=v2 PARAMS="-url http://localhost:8000 -debug $FDV1_SKIP" sh "$HARNESS_RUNNER" + +# FDv2 (v3 harness). Only the final run stops the test service. +FDV2_SUPPRESSIONS="$SCRIPT_DIR/suppressions-fdv2.txt" +FDV2_SKIP="" +if [ -s "$FDV2_SUPPRESSIONS" ]; then + FDV2_SKIP="--skip-from=$FDV2_SUPPRESSIONS" fi -"$REPO_ROOT/sdk-test-harness" \ - -url http://localhost:8000 \ - -debug \ - $EXTRA_ARGS +echo "=== Running FDv2 contract tests ===" +VERSION=v3 PARAMS="-url http://localhost:8000 -debug $FDV2_SKIP -stop-service-at-end" sh "$HARNESS_RUNNER" diff --git a/packages/sdk/react-native/contract-tests/run-contract-tests.sh b/packages/sdk/react-native/contract-tests/run-contract-tests.sh index 48121126bc..3db633c1e7 100755 --- a/packages/sdk/react-native/contract-tests/run-contract-tests.sh +++ b/packages/sdk/react-native/contract-tests/run-contract-tests.sh @@ -37,7 +37,7 @@ echo "Port forwarding configured." echo "" echo "=== Starting adapter ===" cd "$REPO_ROOT" -yarn workspace react-native-contract-test-entity run start:adapter > /tmp/rn-adapter.log 2>&1 & +yarn workspace @launchdarkly/react-native-contract-test-entity run start:adapter > /tmp/rn-adapter.log 2>&1 & ADAPTER_PID=$! echo "Adapter started (PID: $ADAPTER_PID)" diff --git a/packages/sdk/react-native/contract-tests/suppressions-fdv2.txt b/packages/sdk/react-native/contract-tests/suppressions-fdv2.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sdk/react-native/example-fdv2/App.tsx b/packages/sdk/react-native/example-fdv2/App.tsx index b7d9afefec..e667556862 100644 --- a/packages/sdk/react-native/example-fdv2/App.tsx +++ b/packages/sdk/react-native/example-fdv2/App.tsx @@ -14,7 +14,6 @@ const featureClient = new ReactNativeLDClient(LAUNCHDARKLY_MOBILE_KEY, AutoEnvAt id: 'ld-rn-fdv2-test-app', version: '0.0.1', }, - // @ts-ignore dataSystem is @internal dataSystem: {}, }); diff --git a/packages/sdk/react-native/example-fdv2/src/welcome.tsx b/packages/sdk/react-native/example-fdv2/src/welcome.tsx index 95eb25664f..da8a5b55bf 100644 --- a/packages/sdk/react-native/example-fdv2/src/welcome.tsx +++ b/packages/sdk/react-native/example-fdv2/src/welcome.tsx @@ -104,7 +104,6 @@ export default function Welcome() { }; const onSetConnectionMode = (mode?: FDv2ConnectionMode) => { - // @ts-ignore setConnectionMode is @internal - experimental FDv2 opt-in ldc.setConnectionMode(mode); setCurrentMode(mode ?? 'automatic'); }; diff --git a/packages/sdk/react-native/src/RNOptions.ts b/packages/sdk/react-native/src/RNOptions.ts index 902afcff68..99b9ff608c 100644 --- a/packages/sdk/react-native/src/RNOptions.ts +++ b/packages/sdk/react-native/src/RNOptions.ts @@ -137,18 +137,17 @@ export interface RNSpecificOptions { plugins?: LDPlugin[]; /** - * @internal - * - * This feature is experimental and should NOT be considered ready for - * production use. It may change or be removed without notice and is not - * subject to backwards compatibility guarantees. - * * Configuration for the FDv2 data system. When present, the SDK uses * the FDv2 protocol for flag delivery instead of the default FDv1 * protocol. * * Note: Network-based automatic mode switching is not yet supported. * Lifecycle-based switching (foreground/background) is fully functional. + * + * This option is not stable, and not subject to any backwards compatibility + * guarantees or semantic versioning. It is in early access. If you want access + * to this feature please join the EAP. + * https://launchdarkly.com/docs/sdk/features/data-saving-mode */ dataSystem?: RNDataSystemOptions; } diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.ts b/packages/sdk/react-native/src/ReactNativeLDClient.ts index e3c95199fb..e2eec1aea3 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.ts +++ b/packages/sdk/react-native/src/ReactNativeLDClient.ts @@ -242,12 +242,6 @@ export default class ReactNativeLDClient extends LDClientImpl { */ async setConnectionMode(mode: ConnectionMode): Promise; /** - * @internal - * - * This overload is experimental and should NOT be considered ready for - * production use. It may change or be removed without notice and is not - * subject to backwards compatibility guarantees. - * * Sets the connection mode for the FDv2 data system. * * When the FDv2 data system is enabled (`dataSystem` option), this method @@ -255,6 +249,11 @@ export default class ReactNativeLDClient extends LDClientImpl { * `undefined` to clear an explicit override and return to automatic mode * selection. * + * This overload is not stable, and not subject to any backwards compatibility + * guarantees or semantic versioning. It is in early access. If you want access + * to this feature please join the EAP. + * https://launchdarkly.com/docs/sdk/features/data-saving-mode + * * @param mode The connection mode to use, or `undefined` to clear the * override (FDv2 only). */ @@ -290,7 +289,15 @@ export default class ReactNativeLDClient extends LDClientImpl { */ getConnectionMode(): ConnectionMode; /** - * @internal + * Gets the SDK connection mode. + * + * When the FDv2 data system is enabled (`dataSystem` option), the connection + * mode may also be `'one-shot'` or `'background'`. + * + * This overload is not stable, and not subject to any backwards compatibility + * guarantees or semantic versioning. It is in early access. If you want access + * to this feature please join the EAP. + * https://launchdarkly.com/docs/sdk/features/data-saving-mode */ getConnectionMode(): FDv2ConnectionMode; getConnectionMode(): ConnectionMode | FDv2ConnectionMode { diff --git a/packages/shared/sdk-client/src/api/LDOptions.ts b/packages/shared/sdk-client/src/api/LDOptions.ts index 0f166f307a..e7841d07e5 100644 --- a/packages/shared/sdk-client/src/api/LDOptions.ts +++ b/packages/shared/sdk-client/src/api/LDOptions.ts @@ -290,17 +290,14 @@ export interface LDOptions { cleanOldPersistentData?: boolean; /** - * @internal - * - * WARNING: This option is EXPERIMENTAL and UNSUPPORTED. It is subject to - * change or removal without notice. Do not use in production applications. - * Using this option may result in unexpected behavior, data loss, or - * breaking changes in future SDK versions. LaunchDarkly does not provide - * support for configurations using this option. - * * Configuration for the FDv2 data system. When present, the SDK uses * the FDv2 protocol for flag delivery instead of the default FDv1 * protocol. + * + * This option is not stable, and not subject to any backwards compatibility + * guarantees or semantic versioning. It is in early access. If you want access + * to this feature please join the EAP. + * https://launchdarkly.com/docs/sdk/features/data-saving-mode */ dataSystem?: LDClientDataSystemOptions;