diff --git a/.github/workflows/server-node.yml b/.github/workflows/server-node.yml index e216e60add..9932876a6b 100644 --- a/.github/workflows/server-node.yml +++ b/.github/workflows/server-node.yml @@ -31,8 +31,12 @@ jobs: with: workspace_name: '@launchdarkly/node-server-sdk' workspace_path: packages/sdk/server-node + - name: Install contract test service dependencies + run: yarn workspace node-server-sdk-contract-tests install --no-immutable + - name: Build the test service + run: yarn contract-test-service-build - name: Launch the test service in the background - run: yarn run contract-test-service 2>&1 & + run: yarn contract-test-service 2>&1 & - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.0.2 with: test_service_port: 8000 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9b79d778a5..8f31543e86 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,21 +1,21 @@ { - "packages/shared/common": "2.15.0", - "packages/shared/sdk-server": "2.14.0", - "packages/sdk/server-node": "9.8.0", - "packages/sdk/cloudflare": "2.7.3", - "packages/sdk/fastly": "0.1.4", - "packages/shared/sdk-server-edge": "2.6.3", - "packages/sdk/vercel": "1.3.27", - "packages/sdk/akamai-base": "3.0.4", - "packages/sdk/akamai-edgekv": "1.4.6", - "packages/shared/akamai-edgeworker-sdk": "2.0.4", - "packages/store/node-server-sdk-dynamodb": "6.2.8", - "packages/store/node-server-sdk-redis": "4.2.8", - "packages/shared/sdk-client": "1.12.5", - "packages/sdk/react-native": "10.9.8", - "packages/telemetry/node-server-sdk-otel": "1.1.8", - "packages/sdk/browser": "0.5.1", - "packages/sdk/server-ai": "0.9.5", - "packages/telemetry/browser-telemetry": "1.0.4", - "packages/tooling/jest": "0.1.3" + "packages/shared/common": "2.16.0", + "packages/shared/sdk-server": "2.15.0", + "packages/sdk/server-node": "9.9.0", + "packages/sdk/cloudflare": "2.7.4", + "packages/sdk/fastly": "0.1.5", + "packages/shared/sdk-server-edge": "2.6.4", + "packages/sdk/vercel": "1.3.28", + "packages/sdk/akamai-base": "3.0.5", + "packages/sdk/akamai-edgekv": "1.4.7", + "packages/shared/akamai-edgeworker-sdk": "2.0.5", + "packages/store/node-server-sdk-dynamodb": "6.2.9", + "packages/store/node-server-sdk-redis": "4.2.9", + "packages/shared/sdk-client": "1.12.6", + "packages/sdk/react-native": "10.9.9", + "packages/telemetry/node-server-sdk-otel": "1.2.0", + "packages/sdk/browser": "0.5.3", + "packages/sdk/server-ai": "0.9.6", + "packages/telemetry/browser-telemetry": "1.0.6", + "packages/tooling/jest": "0.1.4" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1794aafa5..fdf84e631f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,6 +35,14 @@ To build all projects, from the root directory: yarn build ``` +To build a single project and all of its dependencies: +``` +yarn workspaces foreach -pR --topological-dev --from '@launchdarkly/js-client-sdk' run build +``` +Replacing `@launchdarkly/js-client-sdk` with the specific package you want to build. + +Running `yarn build` in an individual package will build that package, but will not rebuild any dependencies. + ### Testing Unit tests should be implemented in a `__tests__` folder in the root of the package. The directory structure inside of `__tests__` should mirror that of the source directory. diff --git a/contract-tests/BigSegmentTestStore.js b/contract-tests/BigSegmentTestStore.js deleted file mode 100644 index 747f9a8870..0000000000 --- a/contract-tests/BigSegmentTestStore.js +++ /dev/null @@ -1,31 +0,0 @@ -import got from 'got'; - -export default class BigSegmentTestStore { - /** - * Create a big segment test store suitable for use with the contract tests. - * @param {string} callbackUri Uri on the test service to direct big segments - * calls to. - */ - constructor(callbackUri) { - this._callbackUri = callbackUri; - } - - async getMetadata() { - const data = await got.get(`${this._callbackUri}/getMetadata`, { retry: { limit: 0 } }).json(); - return data; - } - - async getUserMembership(contextHash) { - const data = await got - .post(`${this._callbackUri}/getMembership`, { - retry: { limit: 0 }, - json: { - contextHash, - }, - }) - .json(); - return data?.values; - } - - close() {} -} diff --git a/contract-tests/TestHook.js b/contract-tests/TestHook.js deleted file mode 100644 index 548971b367..0000000000 --- a/contract-tests/TestHook.js +++ /dev/null @@ -1,51 +0,0 @@ -import got from 'got'; - -export default class TestHook { - constructor(name, endpoint, data, errors) { - this._name = name; - this._endpoint = endpoint; - this._data = data; - this._errors = errors; - } - - async _safePost(body) { - try { - await got.post(this._endpoint, { json: body }); - } catch { - // The test could move on before the post, so we are ignoring - // failed posts. - } - } - - getMetadata() { - return { - name: 'LaunchDarkly Tracing Hook', - }; - } - - beforeEvaluation(hookContext, data) { - if (this._errors?.beforeEvaluation) { - throw new Error(this._errors.beforeEvaluation); - } - this._safePost({ - evaluationSeriesContext: hookContext, - evaluationSeriesData: data, - stage: 'beforeEvaluation', - }); - return { ...data, ...(this._data?.['beforeEvaluation'] || {}) }; - } - - afterEvaluation(hookContext, data, detail) { - if (this._errors?.afterEvaluation) { - throw new Error(this._errors.afterEvaluation); - } - this._safePost({ - evaluationSeriesContext: hookContext, - evaluationSeriesData: data, - stage: 'afterEvaluation', - evaluationDetail: detail, - }); - - return { ...data, ...(this._data?.['afterEvaluation'] || {}) }; - } -} diff --git a/contract-tests/log.js b/contract-tests/log.js deleted file mode 100644 index e6dcd973d8..0000000000 --- a/contract-tests/log.js +++ /dev/null @@ -1,20 +0,0 @@ -import ld from 'node-server-sdk'; - -export function Log(tag) { - function doLog(level, message) { - console.log(new Date().toISOString() + ` [${tag}] ${level}: ${message}`); - } - return { - info: (message) => doLog('info', message), - error: (message) => doLog('error', message), - }; -} - -export function sdkLogger(tag) { - return ld.basicLogger({ - level: 'debug', - destination: (line) => { - console.log(new Date().toISOString() + ` [${tag}.sdk] ${line}`); - }, - }); -} diff --git a/contract-tests/package.json b/contract-tests/package.json index ef4975c946..7b3f880d64 100644 --- a/contract-tests/package.json +++ b/contract-tests/package.json @@ -1,17 +1,26 @@ { "name": "node-server-sdk-contract-tests", "version": "0.0.0", - "main": "index.js", + "main": "dist/src/index.js", "scripts": { - "start": "node --inspect index.js" + "start": "node --inspect dist/src/index.js", + "build": "tsc", + "dev": "tsc --watch" }, "type": "module", "author": "", "license": "Apache-2.0", + "private": true, "dependencies": { + "@launchdarkly/node-server-sdk": "9.8.0", "body-parser": "^1.19.0", "express": "^4.17.1", - "node-server-sdk": "file:../packages/sdk/server-node", - "got": "13.0.0" + "got": "14.4.7" + }, + "devDependencies": { + "@types/body-parser": "^1.19.2", + "@types/express": "^4.17.13", + "@types/node": "^18.11.9", + "typescript": "^4.9.0" } } diff --git a/contract-tests/src/BigSegmentTestStore.ts b/contract-tests/src/BigSegmentTestStore.ts new file mode 100644 index 0000000000..7e31dc0498 --- /dev/null +++ b/contract-tests/src/BigSegmentTestStore.ts @@ -0,0 +1,40 @@ +import got from 'got'; + +interface BigSegmentMetadata { + lastUpToDate?: number; +} + +interface BigSegmentMembership { + values?: Record; +} + +export default class BigSegmentTestStore { + private _callbackUri: string; + + /** + * Create a big segment test store suitable for use with the contract tests. + * @param callbackUri Uri on the test service to direct big segments calls to. + */ + constructor(callbackUri: string) { + this._callbackUri = callbackUri; + } + + async getMetadata(): Promise { + const data = await got.get(`${this._callbackUri}/getMetadata`, { retry: { limit: 0 } }).json(); + return data as BigSegmentMetadata; + } + + async getUserMembership(contextHash: string): Promise | undefined> { + const data = await got + .post(`${this._callbackUri}/getMembership`, { + retry: { limit: 0 }, + json: { + contextHash, + }, + }) + .json(); + return (data as BigSegmentMembership)?.values; + } + + close(): void {} +} diff --git a/contract-tests/src/TestHook.ts b/contract-tests/src/TestHook.ts new file mode 100644 index 0000000000..abad14331d --- /dev/null +++ b/contract-tests/src/TestHook.ts @@ -0,0 +1,75 @@ +import got from 'got'; + +import { integrations, LDEvaluationDetail } from '@launchdarkly/node-server-sdk'; + +export interface HookData { + beforeEvaluation?: Record; + afterEvaluation?: Record; +} + +export interface HookErrors { + beforeEvaluation?: string; + afterEvaluation?: string; +} + +export default class TestHook implements integrations.Hook { + private _name: string; + private _endpoint: string; + private _data?: HookData; + private _errors?: HookErrors; + + constructor(name: string, endpoint: string, data?: HookData, errors?: HookErrors) { + this._name = name; + this._endpoint = endpoint; + this._data = data; + this._errors = errors; + } + + private async _safePost(body: unknown): Promise { + try { + await got.post(this._endpoint, { json: body }); + } catch { + // The test could move on before the post, so we are ignoring + // failed posts. + } + } + + getMetadata(): integrations.HookMetadata { + return { + name: this._name, + }; + } + + beforeEvaluation( + hookContext: integrations.EvaluationSeriesContext, + data: integrations.EvaluationSeriesData, + ): integrations.EvaluationSeriesData { + if (this._errors?.beforeEvaluation) { + throw new Error(this._errors.beforeEvaluation); + } + this._safePost({ + evaluationSeriesContext: hookContext, + evaluationSeriesData: data, + stage: 'beforeEvaluation', + }); + return { ...data, ...(this._data?.beforeEvaluation || {}) }; + } + + afterEvaluation( + hookContext: integrations.EvaluationSeriesContext, + data: integrations.EvaluationSeriesData, + detail: LDEvaluationDetail, + ): integrations.EvaluationSeriesData { + if (this._errors?.afterEvaluation) { + throw new Error(this._errors.afterEvaluation); + } + this._safePost({ + evaluationSeriesContext: hookContext, + evaluationSeriesData: data, + stage: 'afterEvaluation', + evaluationDetail: detail, + }); + + return { ...data, ...(this._data?.afterEvaluation || {}) }; + } +} diff --git a/contract-tests/index.js b/contract-tests/src/index.ts similarity index 70% rename from contract-tests/index.js rename to contract-tests/src/index.ts index 9b6937fab6..882ff549d5 100644 --- a/contract-tests/index.js +++ b/contract-tests/src/index.ts @@ -1,22 +1,23 @@ import bodyParser from 'body-parser'; -import express from 'express'; +import express, { Request, Response } from 'express'; +import { Server } from 'http'; import { Log } from './log.js'; -import { badCommandError, newSdkClientEntity } from './sdkClientEntity.js'; +import { badCommandError, newSdkClientEntity, SdkClientEntity } from './sdkClientEntity.js'; const app = express(); -let server = null; +let server: Server | null = null; const port = 8000; let clientCounter = 0; -const clients = {}; +const clients: Record = {}; const mainLog = Log('service'); app.use(bodyParser.json()); -app.get('/', (req, res) => { +app.get('/', (req: Request, res: Response) => { res.header('Content-Type', 'application/json'); res.json({ capabilities: [ @@ -45,21 +46,23 @@ app.get('/', (req, res) => { }); }); -app.delete('/', (req, res) => { +app.delete('/', (req: Request, res: Response) => { mainLog.info('Test service has told us to exit'); res.status(204); res.send(); // Defer the following actions till after the response has been sent setTimeout(() => { - server.close(() => process.exit()); + if (server) { + server.close(() => process.exit()); + } // We force-quit with process.exit because, even after closing the server, there could be some // scheduled tasks lingering if an SDK instance didn't get cleaned up properly, and we don't want // that to prevent us from quitting. }, 1); }); -app.post('/', async (req, res) => { +app.post('/', async (req: Request, res: Response) => { const options = req.body; clientCounter += 1; @@ -74,14 +77,14 @@ app.post('/', async (req, res) => { res.set('Location', resourceUrl); } catch (e) { res.status(500); - const message = e.message || JSON.stringify(e); - mainLog.error('Error creating client: ' + message); + const message = e instanceof Error ? e.message : JSON.stringify(e); + mainLog.error(`Error creating client: ${message}`); res.write(message); } res.send(); }); -app.post('/clients/:id', async (req, res) => { +app.post('/clients/:id', async (req: Request, res: Response) => { const client = clients[req.params.id]; if (!client) { res.status(404); @@ -97,8 +100,10 @@ app.post('/clients/:id', async (req, res) => { } catch (e) { const isBadRequest = e === badCommandError; res.status(isBadRequest ? 400 : 500); - res.write(e.message || JSON.stringify(e)); - if (!isBadRequest && e.stack) { + const message = e instanceof Error ? e.message : JSON.stringify(e); + res.write(message); + if (!isBadRequest && e instanceof Error && e.stack) { + // eslint-disable-next-line no-console console.log(e.stack); } } @@ -106,7 +111,7 @@ app.post('/clients/:id', async (req, res) => { res.send(); }); -app.delete('/clients/:id', async (req, res) => { +app.delete('/clients/:id', async (req: Request, res: Response) => { const client = clients[req.params.id]; if (!client) { res.status(404); @@ -120,5 +125,6 @@ app.delete('/clients/:id', async (req, res) => { }); server = app.listen(port, () => { + // eslint-disable-next-line no-console console.log('Listening on port %d', port); }); diff --git a/contract-tests/src/log.ts b/contract-tests/src/log.ts new file mode 100644 index 0000000000..1cb3005bc0 --- /dev/null +++ b/contract-tests/src/log.ts @@ -0,0 +1,27 @@ +import ld from '@launchdarkly/node-server-sdk'; + +export interface Logger { + info: (message: string) => void; + error: (message: string) => void; +} + +export function Log(tag: string): Logger { + function doLog(level: string, message: string): void { + // eslint-disable-next-line no-console + console.log(`${new Date().toISOString()} [${tag}] ${level}: ${message}`); + } + return { + info: (message: string) => doLog('info', message), + error: (message: string) => doLog('error', message), + }; +} + +export function sdkLogger(tag: string): ld.LDLogger { + return ld.basicLogger({ + level: 'debug', + destination: (line: string) => { + // eslint-disable-next-line no-console + console.log(`${new Date().toISOString()} [${tag}.sdk] ${line}`); + }, + }); +} diff --git a/contract-tests/sdkClientEntity.js b/contract-tests/src/sdkClientEntity.ts similarity index 60% rename from contract-tests/sdkClientEntity.js rename to contract-tests/src/sdkClientEntity.ts index f3f10fbf2c..ad0606b1d1 100644 --- a/contract-tests/sdkClientEntity.js +++ b/contract-tests/src/sdkClientEntity.ts @@ -1,12 +1,19 @@ import got from 'got'; + import ld, { createMigration, + LDClient, LDConcurrentExecution, + LDContext, LDExecutionOrdering, + LDFlagValue, LDMigrationError, + LDMigrationStage, LDMigrationSuccess, + LDOptions, LDSerialExecution, -} from 'node-server-sdk'; + LDUser, +} from '@launchdarkly/node-server-sdk'; import BigSegmentTestStore from './BigSegmentTestStore.js'; import { Log, sdkLogger } from './log.js'; @@ -15,29 +22,127 @@ import TestHook from './TestHook.js'; const badCommandError = new Error('unsupported command'); export { badCommandError }; -export function makeSdkConfig(options, tag) { - const cf = { +interface SdkConfigOptions { + streaming?: { + baseUri: string; + initialRetryDelayMs?: number; + filter?: string; + }; + polling?: { + baseUri: string; + pollIntervalMs: number; + filter?: string; + }; + events?: { + allAttributesPrivate?: boolean; + baseUri: string; + capacity?: number; + enableDiagnostics?: boolean; + flushIntervalMs?: number; + globalPrivateAttributes?: string[]; + enableGzip?: boolean; + }; + tags?: { + applicationId: string; + applicationVersion: string; + }; + bigSegments?: { + callbackUri: string; + userCacheSize?: number; + userCacheTimeMs?: number; + statusPollIntervalMs?: number; + staleAfterMs?: number; + }; + hooks?: { + hooks: { + name: string; + callbackUri: string; + data: any; + errors: any; + }[]; + }; + wrapper?: { + name?: string; + version?: string; + }; +} + +interface CommandParams { + command: string; + evaluate?: { + flagKey: string; + context?: LDContext; + user?: LDUser; + defaultValue: LDFlagValue; + detail?: boolean; + valueType?: string; + }; + evaluateAll?: { + context?: LDContext; + user?: LDUser; + clientSideOnly?: boolean; + detailsOnlyForTrackedFlags?: boolean; + withReasons?: boolean; + }; + identifyEvent?: { + context?: LDContext; + user?: LDUser; + }; + customEvent?: { + eventKey: string; + context?: LDContext; + user?: LDUser; + data?: any; + metricValue?: number; + }; + migrationVariation?: { + key: string; + context: LDContext; + defaultStage: LDMigrationStage; + }; + migrationOperation?: { + operation: string; + key: string; + context: LDContext; + defaultStage: LDMigrationStage; + payload: any; + readExecutionOrder: string; + trackLatency?: boolean; + trackErrors?: boolean; + trackConsistency?: boolean; + newEndpoint: string; + oldEndpoint: string; + }; +} + +export function makeSdkConfig(options: SdkConfigOptions, tag: string): LDOptions { + const cf: LDOptions = { logger: sdkLogger(tag), diagnosticOptOut: true, }; - const maybeTime = (seconds) => + const maybeTime = (seconds?: number) => seconds === undefined || seconds === null ? undefined : seconds / 1000; + if (options.streaming) { cf.streamUri = options.streaming.baseUri; cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs); if (options.streaming.filter) { - cf.payloadFilterKey = options.streaming.filter; + cf.application = cf.application || {}; + cf.application.payloadFilterKey = options.streaming.filter; } } + if (options.polling) { cf.stream = false; cf.baseUri = options.polling.baseUri; - cf.pollInterface = options.polling.pollIntervalMs / 1000; + cf.pollInterval = options.polling.pollIntervalMs / 1000; if (options.polling.filter) { - cf.payloadFilterKey = options.polling.filter; + cf.application = cf.application || {}; + cf.application.payloadFilterKey = options.polling.filter; } } + if (options.events) { cf.allAttributesPrivate = options.events.allAttributesPrivate; cf.eventsUri = options.events.baseUri; @@ -47,12 +152,14 @@ export function makeSdkConfig(options, tag) { cf.privateAttributes = options.events.globalPrivateAttributes; cf.enableEventCompression = options.events.enableGzip; } + if (options.tags) { cf.application = { id: options.tags.applicationId, version: options.tags.applicationVersion, }; } + if (options.bigSegments) { const bigSegmentsOptions = options.bigSegments; cf.bigSegments = { @@ -69,11 +176,13 @@ export function makeSdkConfig(options, tag) { : undefined, }; } + if (options.hooks) { cf.hooks = options.hooks.hooks.map( (hook) => new TestHook(hook.name, hook.callbackUri, hook.data, hook.errors), ); } + if (options.wrapper) { if (options.wrapper.name) { cf.wrapperName = options.wrapper.name; @@ -82,10 +191,11 @@ export function makeSdkConfig(options, tag) { cf.wrapperVersion = options.wrapper.version; } } + return cf; } -function getExecution(order) { +function getExecution(order: string) { switch (order) { case 'serial': { return new LDSerialExecution(LDExecutionOrdering.Fixed); @@ -102,29 +212,41 @@ function getExecution(order) { } } -function makeMigrationPostOptions(payload) { +function makeMigrationPostOptions(payload: any) { if (payload) { return { body: payload }; } return {}; } -export async function newSdkClientEntity(options) { - const c = {}; +function contextOrUser( + context: LDContext | undefined, + user: LDUser | undefined, +): LDContext | LDUser { + return (context || user)!; +} + +export interface SdkClientEntity { + close: () => void; + doCommand: (params: CommandParams) => Promise; +} + +export async function newSdkClientEntity(options: any): Promise { + const c: any = {}; const log = Log(options.tag); - log.info('Creating client with configuration: ' + JSON.stringify(options.configuration)); + log.info(`Creating client with configuration: ${JSON.stringify(options.configuration)}`); const timeout = options.configuration.startWaitTimeMs !== null && options.configuration.startWaitTimeMs !== undefined ? options.configuration.startWaitTimeMs : 5000; - const client = ld.init( + const client: LDClient = ld.init( options.configuration.credential || 'unknown-sdk-key', makeSdkConfig(options.configuration, options.tag), ); try { - await client.waitForInitialization({ timeout: timeout }); + await client.waitForInitialization({ timeout }); } catch (_) { // if waitForInitialization() rejects, the client failed to initialize, see next line } @@ -138,36 +260,25 @@ export async function newSdkClientEntity(options) { log.info('Test ended'); }; - c.doCommand = async (params) => { - log.info('Received command: ' + params.command); + c.doCommand = async (params: CommandParams) => { + log.info(`Received command: ${params.command}`); switch (params.command) { case 'evaluate': { - const pe = params.evaluate; + const pe = params.evaluate!; + const context = contextOrUser(pe.context, pe.user); if (pe.detail) { switch (pe.valueType) { case 'bool': - return await client.boolVariationDetail( - pe.flagKey, - pe.context || pe.user, - pe.defaultValue, - ); + return client.boolVariationDetail(pe.flagKey, context, pe.defaultValue); case 'int': // Intentional fallthrough. case 'double': - return await client.numberVariationDetail( - pe.flagKey, - pe.context || pe.user, - pe.defaultValue, - ); + return client.numberVariationDetail(pe.flagKey, context, pe.defaultValue); case 'string': - return await client.stringVariationDetail( - pe.flagKey, - pe.context || pe.user, - pe.defaultValue, - ); + return client.stringVariationDetail(pe.flagKey, context, pe.defaultValue); default: - return await client.variationDetail( + return client.variationDetail( pe.flagKey, - pe.context || pe.user, + contextOrUser(pe.context, pe.user), pe.defaultValue, ); } @@ -175,54 +286,42 @@ export async function newSdkClientEntity(options) { switch (pe.valueType) { case 'bool': return { - value: await client.boolVariation( - pe.flagKey, - pe.context || pe.user, - pe.defaultValue, - ), + value: await client.boolVariation(pe.flagKey, context, pe.defaultValue), }; case 'int': // Intentional fallthrough. case 'double': return { - value: await client.numberVariation( - pe.flagKey, - pe.context || pe.user, - pe.defaultValue, - ), + value: await client.numberVariation(pe.flagKey, context, pe.defaultValue), }; case 'string': return { - value: await client.stringVariation( - pe.flagKey, - pe.context || pe.user, - pe.defaultValue, - ), + value: await client.stringVariation(pe.flagKey, context, pe.defaultValue), }; default: return { - value: await client.variation(pe.flagKey, pe.context || pe.user, pe.defaultValue), + value: await client.variation(pe.flagKey, context, pe.defaultValue), }; } } } case 'evaluateAll': { - const pea = params.evaluateAll; + const pea = params.evaluateAll!; const eao = { clientSideOnly: pea.clientSideOnly, detailsOnlyForTrackedFlags: pea.detailsOnlyForTrackedFlags, withReasons: pea.withReasons, }; - return { state: await client.allFlagsState(pea.context || pea.user, eao) }; + return { state: await client.allFlagsState(contextOrUser(pea.context, pea.user), eao) }; } case 'identifyEvent': - client.identify(params.identifyEvent.context || params.identifyEvent.user); + client.identify(params.identifyEvent!.context || params.identifyEvent!.user!); return undefined; case 'customEvent': { - const pce = params.customEvent; - client.track(pce.eventKey, pce.context || pce.user, pce.data, pce.metricValue); + const pce = params.customEvent!; + client.track(pce.eventKey, contextOrUser(pce.context, pce.user), pce.data, pce.metricValue); return undefined; } @@ -231,20 +330,21 @@ export async function newSdkClientEntity(options) { return undefined; case 'getBigSegmentStoreStatus': - return await client.bigSegmentStoreStatusProvider.requireStatus(); + return client.bigSegmentStoreStatusProvider.requireStatus(); - case 'migrationVariation': - const migrationVariation = params.migrationVariation; + case 'migrationVariation': { + const migrationVariation = params.migrationVariation!; const res = await client.migrationVariation( migrationVariation.key, migrationVariation.context, migrationVariation.defaultStage, ); return { result: res.value }; + } - case 'migrationOperation': - const migrationOperation = params.migrationOperation; - const readExecutionOrder = migrationOperation.readExecutionOrder; + case 'migrationOperation': { + const migrationOperation = params.migrationOperation!; + const { readExecutionOrder } = migrationOperation; const migration = createMigration(client, { execution: getExecution(readExecutionOrder), @@ -258,7 +358,7 @@ export async function newSdkClientEntity(options) { makeMigrationPostOptions(payload), ); return LDMigrationSuccess(res.body); - } catch (err) { + } catch (err: any) { return LDMigrationError(err.message); } }, @@ -269,7 +369,7 @@ export async function newSdkClientEntity(options) { makeMigrationPostOptions(payload), ); return LDMigrationSuccess(res.body); - } catch (err) { + } catch (err: any) { return LDMigrationError(err.message); } }, @@ -280,7 +380,7 @@ export async function newSdkClientEntity(options) { makeMigrationPostOptions(payload), ); return LDMigrationSuccess(res.body); - } catch (err) { + } catch (err: any) { return LDMigrationError(err.message); } }, @@ -291,7 +391,7 @@ export async function newSdkClientEntity(options) { makeMigrationPostOptions(payload), ); return LDMigrationSuccess(res.body); - } catch (err) { + } catch (err: any) { return LDMigrationError(err.message); } }, @@ -307,9 +407,8 @@ export async function newSdkClientEntity(options) { ); if (res.success) { return { result: res.result }; - } else { - return { result: res.error }; } + return { result: res.error }; } case 'write': { const res = await migration.write( @@ -321,12 +420,14 @@ export async function newSdkClientEntity(options) { if (res.authoritative.success) { return { result: res.authoritative.result }; - } else { - return { result: res.authoritative.error }; } + return { result: res.authoritative.error }; + } + default: { + return undefined; } } - return undefined; + } default: throw badCommandError; diff --git a/contract-tests/testharness-suppressions.txt b/contract-tests/testharness-suppressions.txt index 8a29da94d7..0fa78cd1b0 100644 --- a/contract-tests/testharness-suppressions.txt +++ b/contract-tests/testharness-suppressions.txt @@ -1,2 +1,6 @@ streaming/validation/drop and reconnect if stream event has malformed JSON streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema +streaming/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has no trailing slash/GET +streaming/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET +polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has no trailing slash/GET +polling/requests/URL path is computed correctly/environment_filter_key="encoding_not_necessary"/base URI has a trailing slash/GET diff --git a/contract-tests/tsconfig.json b/contract-tests/tsconfig.json new file mode 100644 index 0000000000..50aee6cff3 --- /dev/null +++ b/contract-tests/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + // Uses "." so it can load package.json. + "rootDir": ".", + "outDir": "dist", + "target": "ES2020", + "lib": ["ES2020"], + "module": "Node16", + "moduleResolution": "node16", + "strict": true, + "noImplicitOverride": true, + // Needed for CommonJS modules: markdown-it, fs-extra + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "declaration": true, + "declarationMap": true, // enables importers to jump to source + "resolveJsonModule": true, + "stripInternal": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__"] +} diff --git a/contract-tests/tsconfig.ref.json b/contract-tests/tsconfig.ref.json new file mode 100644 index 0000000000..34a1cb607a --- /dev/null +++ b/contract-tests/tsconfig.ref.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*", "package.json"], + "compilerOptions": { + "composite": true + } +} diff --git a/package.json b/package.json index 65d85b770b..cf21e229fd 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "packages/sdk/server-ai", "packages/sdk/server-ai/examples/bedrock", "packages/sdk/server-ai/examples/openai", - "packages/telemetry/browser-telemetry" + "packages/telemetry/browser-telemetry", + "contract-tests" ], "private": true, "scripts": { @@ -44,9 +45,10 @@ "lint:fix": "yarn run lint -- --fix", "test": "echo Please run tests for individual packages.", "coverage": "npm run test -- --coverage", - "contract-test-service": "npm --prefix contract-tests install && npm --prefix contract-tests start", + "contract-test-service-build": "yarn workspaces foreach -pR --topological-dev --from 'node-server-sdk-contract-tests' run build", + "contract-test-service": "yarn workspace node-server-sdk-contract-tests start", "contract-test-harness": "curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/master/downloader/run.sh \\ | VERSION=v2 PARAMS=\"-url http://localhost:8000 -debug -stop-service-at-end $TEST_HARNESS_PARAMS\" sh", - "contract-tests": "npm run contract-test-service & npm run contract-test-harness", + "contract-tests": "yarn contract-test-service-build && yarn contract-test-service & yarn contract-test-harness", "prettier": "npx prettier --write \"**/*.{js,ts,tsx,json,yaml,yml,md}\" --log-level warn", "check": "yarn && yarn prettier && yarn lint && tsc && yarn build" }, diff --git a/packages/sdk/akamai-base/CHANGELOG.md b/packages/sdk/akamai-base/CHANGELOG.md index a5dc51b77f..40b71857c8 100644 --- a/packages/sdk/akamai-base/CHANGELOG.md +++ b/packages/sdk/akamai-base/CHANGELOG.md @@ -30,6 +30,16 @@ All notable changes to the LaunchDarkly SDK for Akamai Workers will be documente * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.1 to ^1.1.2 * @launchdarkly/js-server-sdk-common bumped from ^2.2.1 to ^2.2.2 +## [3.0.5](https://github.com/launchdarkly/js-core/compare/akamai-server-base-sdk-v3.0.4...akamai-server-base-sdk-v3.0.5) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^2.0.4 to ^2.0.5 + * @launchdarkly/js-server-sdk-common bumped from ^2.14.0 to ^2.15.0 + ## [3.0.4](https://github.com/launchdarkly/js-core/compare/akamai-server-base-sdk-v3.0.3...akamai-server-base-sdk-v3.0.4) (2025-04-08) diff --git a/packages/sdk/akamai-base/example/package.json b/packages/sdk/akamai-base/example/package.json index f476814c7e..5e6891d949 100644 --- a/packages/sdk/akamai-base/example/package.json +++ b/packages/sdk/akamai-base/example/package.json @@ -32,6 +32,6 @@ "typescript": "5.1.6" }, "dependencies": { - "@launchdarkly/akamai-server-base-sdk": "3.0.4" + "@launchdarkly/akamai-server-base-sdk": "3.0.5" } } diff --git a/packages/sdk/akamai-base/package.json b/packages/sdk/akamai-base/package.json index c10dbdf593..aa8b584595 100644 --- a/packages/sdk/akamai-base/package.json +++ b/packages/sdk/akamai-base/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/akamai-server-base-sdk", - "version": "3.0.4", + "version": "3.0.5", "description": "Akamai LaunchDarkly EdgeWorker SDK", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/akamai-base", "repository": { @@ -73,7 +73,7 @@ "typescript": "5.1.6" }, "dependencies": { - "@launchdarkly/akamai-edgeworker-sdk-common": "^2.0.4", - "@launchdarkly/js-server-sdk-common": "^2.14.0" + "@launchdarkly/akamai-edgeworker-sdk-common": "^2.0.5", + "@launchdarkly/js-server-sdk-common": "^2.15.0" } } diff --git a/packages/sdk/akamai-edgekv/CHANGELOG.md b/packages/sdk/akamai-edgekv/CHANGELOG.md index f8e4bf7a59..dfe17de3c4 100644 --- a/packages/sdk/akamai-edgekv/CHANGELOG.md +++ b/packages/sdk/akamai-edgekv/CHANGELOG.md @@ -31,6 +31,16 @@ All notable changes to the LaunchDarkly SDK for Akamai Workers will be documente * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.1 to ^1.1.2 * @launchdarkly/js-server-sdk-common bumped from ^2.2.1 to ^2.2.2 +## [1.4.7](https://github.com/launchdarkly/js-core/compare/akamai-server-edgekv-sdk-v1.4.6...akamai-server-edgekv-sdk-v1.4.7) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^2.0.4 to ^2.0.5 + * @launchdarkly/js-server-sdk-common bumped from ^2.14.0 to ^2.15.0 + ## [1.4.6](https://github.com/launchdarkly/js-core/compare/akamai-server-edgekv-sdk-v1.4.5...akamai-server-edgekv-sdk-v1.4.6) (2025-04-08) diff --git a/packages/sdk/akamai-edgekv/example/package.json b/packages/sdk/akamai-edgekv/example/package.json index de343e9357..575b5bcf68 100644 --- a/packages/sdk/akamai-edgekv/example/package.json +++ b/packages/sdk/akamai-edgekv/example/package.json @@ -31,6 +31,6 @@ "typescript": "5.1.6" }, "dependencies": { - "@launchdarkly/akamai-server-edgekv-sdk": "1.4.6" + "@launchdarkly/akamai-server-edgekv-sdk": "1.4.7" } } diff --git a/packages/sdk/akamai-edgekv/package.json b/packages/sdk/akamai-edgekv/package.json index 1433ca8c33..c4843de1b5 100644 --- a/packages/sdk/akamai-edgekv/package.json +++ b/packages/sdk/akamai-edgekv/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/akamai-server-edgekv-sdk", - "version": "1.4.6", + "version": "1.4.7", "description": "Akamai LaunchDarkly EdgeWorker SDK for EdgeKV feature store", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/akamai-edgekv", "repository": { @@ -73,7 +73,7 @@ "typescript": "5.1.6" }, "dependencies": { - "@launchdarkly/akamai-edgeworker-sdk-common": "^2.0.4", - "@launchdarkly/js-server-sdk-common": "^2.14.0" + "@launchdarkly/akamai-edgeworker-sdk-common": "^2.0.5", + "@launchdarkly/js-server-sdk-common": "^2.15.0" } } diff --git a/packages/sdk/browser/CHANGELOG.md b/packages/sdk/browser/CHANGELOG.md index 132d3f6c5d..9d7179021e 100644 --- a/packages/sdk/browser/CHANGELOG.md +++ b/packages/sdk/browser/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [0.5.3](https://github.com/launchdarkly/js-core/compare/js-client-sdk-v0.5.2...js-client-sdk-v0.5.3) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-client-sdk-common bumped from 1.12.5 to 1.12.6 + +## [0.5.2](https://github.com/launchdarkly/js-core/compare/js-client-sdk-v0.5.1...js-client-sdk-v0.5.2) (2025-04-15) + + +### Bug Fixes + +* Handle default flush interval for browser SDK. ([#822](https://github.com/launchdarkly/js-core/issues/822)) ([2c1cc7a](https://github.com/launchdarkly/js-core/commit/2c1cc7a117fd011a329dfcc5332fddf7fd11eff9)) + ## [0.5.1](https://github.com/launchdarkly/js-core/compare/js-client-sdk-v0.5.0...js-client-sdk-v0.5.1) (2025-04-08) diff --git a/packages/sdk/browser/__tests__/options.test.ts b/packages/sdk/browser/__tests__/options.test.ts index cb1d84a67f..d8aefc63b1 100644 --- a/packages/sdk/browser/__tests__/options.test.ts +++ b/packages/sdk/browser/__tests__/options.test.ts @@ -2,7 +2,7 @@ import { jest } from '@jest/globals'; import { LDLogger } from '@launchdarkly/js-client-sdk-common'; -import validateOptions, { filterToBaseOptions } from '../src/options'; +import validateBrowserOptions, { filterToBaseOptionsWithDefaults } from '../src/options'; let logger: LDLogger; @@ -16,7 +16,7 @@ beforeEach(() => { }); it('logs no warnings when all configuration is valid', () => { - validateOptions( + validateBrowserOptions( { fetchGoals: true, eventUrlTransformer: (url: string) => url, @@ -31,7 +31,7 @@ it('logs no warnings when all configuration is valid', () => { }); it('warns for invalid configuration', () => { - validateOptions( + validateBrowserOptions( { // @ts-ignore fetchGoals: 'yes', @@ -50,8 +50,8 @@ it('warns for invalid configuration', () => { ); }); -it('applies default options', () => { - const opts = validateOptions({}, logger); +it('applies default browser-specific options', () => { + const opts = validateBrowserOptions({}, logger); expect(opts.fetchGoals).toBe(true); expect(opts.eventUrlTransformer).toBeDefined(); @@ -69,9 +69,24 @@ it('filters to base options', () => { eventUrlTransformer: (url: string) => url, }; - const baseOpts = filterToBaseOptions(opts); + const baseOpts = filterToBaseOptionsWithDefaults(opts); expect(baseOpts.debug).toBe(false); - expect(Object.keys(baseOpts).length).toEqual(1); + expect(Object.keys(baseOpts).length).toEqual(2); expect(baseOpts).not.toHaveProperty('fetchGoals'); expect(baseOpts).not.toHaveProperty('eventUrlTransformer'); + expect(baseOpts.flushInterval).toEqual(2); +}); + +it('applies default overrides to common config flushInterval', () => { + const opts = {}; + const result = filterToBaseOptionsWithDefaults(opts); + expect(result.flushInterval).toEqual(2); +}); + +it('does not override common config flushInterval if it is set', () => { + const opts = { + flushInterval: 15, + }; + const result = filterToBaseOptionsWithDefaults(opts); + expect(result.flushInterval).toEqual(15); }); diff --git a/packages/sdk/browser/contract-tests/suppressions.txt b/packages/sdk/browser/contract-tests/suppressions.txt index a4b3ab831b..18abe27603 100644 --- a/packages/sdk/browser/contract-tests/suppressions.txt +++ b/packages/sdk/browser/contract-tests/suppressions.txt @@ -15,4 +15,4 @@ tags/stream requests/{"applicationId":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL tags/stream requests/{"applicationId":"________________________________________________________________","applicationVersion":null} tags/stream requests/{"applicationId":"________________________________________________________________","applicationVersion":""} tags/stream requests/{"applicationId":"________________________________________________________________","applicationVersion":"._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678"} -tags/stream requests/{"applicationId":"________________________________________________________________","applicationVersion":"________________________________________________________________"} \ No newline at end of file +tags/stream requests/{"applicationId":"________________________________________________________________","applicationVersion":"________________________________________________________________"} diff --git a/packages/sdk/browser/package.json b/packages/sdk/browser/package.json index adc0773a7a..d61a02f273 100644 --- a/packages/sdk/browser/package.json +++ b/packages/sdk/browser/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/js-client-sdk", - "version": "0.5.1", + "version": "0.5.3", "description": "LaunchDarkly SDK for JavaScript in Browsers", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/browser", "repository": { @@ -55,7 +55,7 @@ "check": "yarn prettier && yarn lint && yarn build && yarn test" }, "dependencies": { - "@launchdarkly/js-client-sdk-common": "1.12.5" + "@launchdarkly/js-client-sdk-common": "1.12.6" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 0ef3490e99..1585bdee24 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -21,7 +21,7 @@ import { BrowserIdentifyOptions as LDIdentifyOptions } from './BrowserIdentifyOp import { registerStateDetection } from './BrowserStateDetector'; import GoalManager from './goals/GoalManager'; import { Goal, isClick } from './goals/Goals'; -import validateOptions, { BrowserOptions, filterToBaseOptions } from './options'; +import validateBrowserOptions, { BrowserOptions, filterToBaseOptionsWithDefaults } from './options'; import BrowserPlatform from './platform/BrowserPlatform'; /** @@ -116,13 +116,16 @@ export class BrowserClient extends LDClientImpl implements LDClient { const baseUrl = options.baseUri ?? 'https://clientsdk.launchdarkly.com'; const platform = overridePlatform ?? new BrowserPlatform(logger); - const validatedBrowserOptions = validateOptions(options, logger); + // Only the browser-specific options are in validatedBrowserOptions. + const validatedBrowserOptions = validateBrowserOptions(options, logger); + // The base options are in baseOptionsWithDefaults. + const baseOptionsWithDefaults = filterToBaseOptionsWithDefaults({ ...options, logger }); const { eventUrlTransformer } = validatedBrowserOptions; super( clientSideId, autoEnvAttributes, platform, - filterToBaseOptions({ ...options, logger }), + baseOptionsWithDefaults, ( flagManager: FlagManager, configuration: Configuration, diff --git a/packages/sdk/browser/src/options.ts b/packages/sdk/browser/src/options.ts index 89e53a4c82..afe1bbf66f 100644 --- a/packages/sdk/browser/src/options.ts +++ b/packages/sdk/browser/src/options.ts @@ -73,8 +73,14 @@ const validators: { [Property in keyof BrowserOptions]: TypeValidator | undefine streaming: TypeValidators.Boolean, }; -export function filterToBaseOptions(opts: BrowserOptions): LDOptionsBase { - const baseOptions: LDOptionsBase = { ...opts }; +function withBrowserDefaults(opts: BrowserOptions): BrowserOptions { + const output = { ...opts }; + output.flushInterval ??= DEFAULT_FLUSH_INTERVAL_SECONDS; + return output; +} + +export function filterToBaseOptionsWithDefaults(opts: BrowserOptions): LDOptionsBase { + const baseOptions: LDOptionsBase = withBrowserDefaults(opts); // Remove any browser specific configuration keys so we don't get warnings from // the base implementation for unknown configuration. @@ -84,14 +90,11 @@ export function filterToBaseOptions(opts: BrowserOptions): LDOptionsBase { return baseOptions; } -function applyBrowserDefaults(opts: BrowserOptions) { - // eslint-disable-next-line no-param-reassign - opts.flushInterval ??= DEFAULT_FLUSH_INTERVAL_SECONDS; -} - -export default function validateOptions(opts: BrowserOptions, logger: LDLogger): ValidatedOptions { +export default function validateBrowserOptions( + opts: BrowserOptions, + logger: LDLogger, +): ValidatedOptions { const output: ValidatedOptions = { ...optDefaults }; - applyBrowserDefaults(output); Object.entries(validators).forEach((entry) => { const [key, validator] = entry as [keyof BrowserOptions, TypeValidator]; diff --git a/packages/sdk/cloudflare/CHANGELOG.md b/packages/sdk/cloudflare/CHANGELOG.md index cd14d19e9e..c0463a3f80 100644 --- a/packages/sdk/cloudflare/CHANGELOG.md +++ b/packages/sdk/cloudflare/CHANGELOG.md @@ -21,6 +21,15 @@ All notable changes to the LaunchDarkly SDK for Cloudflare Workers will be docum * devDependencies * @launchdarkly/js-server-sdk-common-edge bumped from 2.2.1 to 2.2.2 +## [2.7.4](https://github.com/launchdarkly/js-core/compare/cloudflare-server-sdk-v2.7.3...cloudflare-server-sdk-v2.7.4) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common-edge bumped from 2.6.3 to 2.6.4 + ## [2.7.3](https://github.com/launchdarkly/js-core/compare/cloudflare-server-sdk-v2.7.2...cloudflare-server-sdk-v2.7.3) (2025-04-08) diff --git a/packages/sdk/cloudflare/example/package.json b/packages/sdk/cloudflare/example/package.json index c8b3446db2..82bffc8fe7 100644 --- a/packages/sdk/cloudflare/example/package.json +++ b/packages/sdk/cloudflare/example/package.json @@ -5,7 +5,7 @@ "module": "./dist/index.mjs", "packageManager": "yarn@3.4.1", "dependencies": { - "@launchdarkly/cloudflare-server-sdk": "2.7.3" + "@launchdarkly/cloudflare-server-sdk": "2.7.4" }, "devDependencies": { "@cloudflare/workers-types": "^4.20230321.0", diff --git a/packages/sdk/cloudflare/jsr.json b/packages/sdk/cloudflare/jsr.json index 201c08d9e0..22a896e1c5 100644 --- a/packages/sdk/cloudflare/jsr.json +++ b/packages/sdk/cloudflare/jsr.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/cloudflare-server-sdk", - "version": "2.7.3", + "version": "2.7.4", "exports": "./src/index.ts", "publish": { "include": [ diff --git a/packages/sdk/cloudflare/package.json b/packages/sdk/cloudflare/package.json index 8f8a76064a..0a2fc11b3d 100644 --- a/packages/sdk/cloudflare/package.json +++ b/packages/sdk/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/cloudflare-server-sdk", - "version": "2.7.3", + "version": "2.7.4", "description": "Cloudflare LaunchDarkly SDK", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/cloudflare", "repository": { @@ -41,7 +41,7 @@ }, "dependencies": { "@cloudflare/workers-types": "^4.20230321.0", - "@launchdarkly/js-server-sdk-common-edge": "2.6.3", + "@launchdarkly/js-server-sdk-common-edge": "2.6.4", "crypto-js": "^4.1.1" }, "devDependencies": { diff --git a/packages/sdk/cloudflare/src/createPlatformInfo.ts b/packages/sdk/cloudflare/src/createPlatformInfo.ts index 1b8a8cc3fa..adf3bade99 100644 --- a/packages/sdk/cloudflare/src/createPlatformInfo.ts +++ b/packages/sdk/cloudflare/src/createPlatformInfo.ts @@ -1,7 +1,7 @@ import type { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common-edge'; const name = '@launchdarkly/cloudflare-server-sdk'; -const version = '2.7.3'; // x-release-please-version +const version = '2.7.4'; // x-release-please-version class CloudflarePlatformInfo implements Info { platformData(): PlatformData { diff --git a/packages/sdk/fastly/CHANGELOG.md b/packages/sdk/fastly/CHANGELOG.md index c3cdb274b7..e7bb918d0d 100644 --- a/packages/sdk/fastly/CHANGELOG.md +++ b/packages/sdk/fastly/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.1.5](https://github.com/launchdarkly/js-core/compare/fastly-server-sdk-v0.1.4...fastly-server-sdk-v0.1.5) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from 2.14.0 to 2.15.0 + ## [0.1.4](https://github.com/launchdarkly/js-core/compare/fastly-server-sdk-v0.1.3...fastly-server-sdk-v0.1.4) (2025-04-08) diff --git a/packages/sdk/fastly/__tests__/createPlatformInfo.test.ts b/packages/sdk/fastly/__tests__/createPlatformInfo.test.ts index 7306f242be..6fef56b7e2 100644 --- a/packages/sdk/fastly/__tests__/createPlatformInfo.test.ts +++ b/packages/sdk/fastly/__tests__/createPlatformInfo.test.ts @@ -1,6 +1,6 @@ import createPlatformInfo from '../src/createPlatformInfo'; -const version = '0.1.4'; // x-release-please-version +const version = '0.1.5'; // x-release-please-version describe('Fastly Platform Info', () => { it('platformData shows correct information', () => { diff --git a/packages/sdk/fastly/example/package.json b/packages/sdk/fastly/example/package.json index 5700df0327..9f13207dab 100644 --- a/packages/sdk/fastly/example/package.json +++ b/packages/sdk/fastly/example/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@fastly/js-compute": "^3.30.1", - "@launchdarkly/fastly-server-sdk": "0.1.4" + "@launchdarkly/fastly-server-sdk": "0.1.5" }, "scripts": { "clean": "rimraf build && rimraf bin", diff --git a/packages/sdk/fastly/package.json b/packages/sdk/fastly/package.json index 765cdcf0d1..52ba362b81 100644 --- a/packages/sdk/fastly/package.json +++ b/packages/sdk/fastly/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/fastly-server-sdk", - "version": "0.1.4", + "version": "0.1.5", "packageManager": "yarn@3.4.1", "description": "Fastly LaunchDarkly SDK", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/fastly", @@ -47,7 +47,7 @@ }, "dependencies": { "@fastly/js-compute": "^3.30.1", - "@launchdarkly/js-server-sdk-common": "2.14.0", + "@launchdarkly/js-server-sdk-common": "2.15.0", "crypto-js": "^4.2.0" }, "devDependencies": { diff --git a/packages/sdk/fastly/src/createPlatformInfo.ts b/packages/sdk/fastly/src/createPlatformInfo.ts index 7b9867fa14..1b9c83e4b5 100644 --- a/packages/sdk/fastly/src/createPlatformInfo.ts +++ b/packages/sdk/fastly/src/createPlatformInfo.ts @@ -1,7 +1,7 @@ import { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common'; const name = '@launchdarkly/fastly-server-sdk'; -const version = '0.1.4'; // x-release-please-version +const version = '0.1.5'; // x-release-please-version class FastlyPlatformInfo implements Info { platformData(): PlatformData { diff --git a/packages/sdk/react-native/CHANGELOG.md b/packages/sdk/react-native/CHANGELOG.md index 28376b4851..68f6b25fa6 100644 --- a/packages/sdk/react-native/CHANGELOG.md +++ b/packages/sdk/react-native/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [10.9.9](https://github.com/launchdarkly/js-core/compare/react-native-client-sdk-v10.9.8...react-native-client-sdk-v10.9.9) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-client-sdk-common bumped from 1.12.5 to 1.12.6 + ## [10.9.8](https://github.com/launchdarkly/js-core/compare/react-native-client-sdk-v10.9.7...react-native-client-sdk-v10.9.8) (2025-04-08) diff --git a/packages/sdk/react-native/package.json b/packages/sdk/react-native/package.json index 32b3a3606a..76cede1a7b 100644 --- a/packages/sdk/react-native/package.json +++ b/packages/sdk/react-native/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/react-native-client-sdk", - "version": "10.9.8", + "version": "10.9.9", "description": "React Native LaunchDarkly SDK", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/react-native", "repository": { @@ -41,7 +41,7 @@ "react-native": "*" }, "dependencies": { - "@launchdarkly/js-client-sdk-common": "1.12.5", + "@launchdarkly/js-client-sdk-common": "1.12.6", "@react-native-async-storage/async-storage": "^1.21.0", "base64-js": "^1.5.1" }, diff --git a/packages/sdk/server-ai/CHANGELOG.md b/packages/sdk/server-ai/CHANGELOG.md index 6f701d1091..02aa576d60 100644 --- a/packages/sdk/server-ai/CHANGELOG.md +++ b/packages/sdk/server-ai/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.9.6](https://github.com/launchdarkly/js-core/compare/server-sdk-ai-v0.9.5...server-sdk-ai-v0.9.6) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/js-server-sdk-common bumped from 2.14.0 to 2.15.0 + * peerDependencies + * @launchdarkly/js-server-sdk-common bumped from 2.x to 2.15.0 + ## [0.9.5](https://github.com/launchdarkly/js-core/compare/server-sdk-ai-v0.9.4...server-sdk-ai-v0.9.5) (2025-04-08) diff --git a/packages/sdk/server-ai/examples/bedrock/package.json b/packages/sdk/server-ai/examples/bedrock/package.json index 2a096185bc..dabb67bf51 100644 --- a/packages/sdk/server-ai/examples/bedrock/package.json +++ b/packages/sdk/server-ai/examples/bedrock/package.json @@ -24,7 +24,7 @@ "dependencies": { "@aws-sdk/client-bedrock-runtime": "^3.679.0", "@launchdarkly/node-server-sdk": "^9.7.1", - "@launchdarkly/server-sdk-ai": "0.9.5" + "@launchdarkly/server-sdk-ai": "0.9.6" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.1.1", diff --git a/packages/sdk/server-ai/examples/openai/package.json b/packages/sdk/server-ai/examples/openai/package.json index 79d6ac63c6..7660f0764d 100644 --- a/packages/sdk/server-ai/examples/openai/package.json +++ b/packages/sdk/server-ai/examples/openai/package.json @@ -22,7 +22,7 @@ "license": "Apache-2.0", "dependencies": { "@launchdarkly/node-server-sdk": "^9.7.1", - "@launchdarkly/server-sdk-ai": "0.9.5", + "@launchdarkly/server-sdk-ai": "0.9.6", "openai": "^4.58.1" }, "devDependencies": { diff --git a/packages/sdk/server-ai/package.json b/packages/sdk/server-ai/package.json index 1bf24454e6..741d919f79 100644 --- a/packages/sdk/server-ai/package.json +++ b/packages/sdk/server-ai/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/server-sdk-ai", - "version": "0.9.5", + "version": "0.9.6", "description": "LaunchDarkly AI SDK for Server-Side JavaScript", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/server-ai", "repository": { @@ -29,7 +29,7 @@ "mustache": "^4.2.0" }, "devDependencies": { - "@launchdarkly/js-server-sdk-common": "2.14.0", + "@launchdarkly/js-server-sdk-common": "2.15.0", "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/jest": "^29.5.3", "@types/mustache": "^4.2.5", diff --git a/packages/sdk/server-node/CHANGELOG.md b/packages/sdk/server-node/CHANGELOG.md index 07997d892b..bbb626006e 100644 --- a/packages/sdk/server-node/CHANGELOG.md +++ b/packages/sdk/server-node/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to `@launchdarkly/node-server-sdk` will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [9.9.0](https://github.com/launchdarkly/js-core/compare/node-server-sdk-v9.8.0...node-server-sdk-v9.9.0) (2025-04-16) + + +### Features + +* Environment ID support for hooks ([#823](https://github.com/launchdarkly/js-core/issues/823)) ([63dc9f9](https://github.com/launchdarkly/js-core/commit/63dc9f9f1300c598e79be27909f8195ac66d54ef)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from 2.14.0 to 2.15.0 + ## [9.8.0](https://github.com/launchdarkly/js-core/compare/node-server-sdk-v9.7.7...node-server-sdk-v9.8.0) (2025-04-08) diff --git a/packages/sdk/server-node/package.json b/packages/sdk/server-node/package.json index 96a90bb69f..b6cf46e9c8 100644 --- a/packages/sdk/server-node/package.json +++ b/packages/sdk/server-node/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/node-server-sdk", - "version": "9.8.0", + "version": "9.9.0", "description": "LaunchDarkly Server-Side SDK for Node.js", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/server-node", "repository": { @@ -45,9 +45,9 @@ }, "license": "Apache-2.0", "dependencies": { - "@launchdarkly/js-server-sdk-common": "2.14.0", + "@launchdarkly/js-server-sdk-common": "2.15.0", "https-proxy-agent": "^5.0.1", - "launchdarkly-eventsource": "2.0.3" + "launchdarkly-eventsource": "2.1.0" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.1.1", diff --git a/packages/sdk/vercel/CHANGELOG.md b/packages/sdk/vercel/CHANGELOG.md index ffe341d808..60de86fdec 100644 --- a/packages/sdk/vercel/CHANGELOG.md +++ b/packages/sdk/vercel/CHANGELOG.md @@ -20,6 +20,15 @@ All notable changes to the LaunchDarkly SDK for Vercel Edge Config will be docum * dependencies * @launchdarkly/js-server-sdk-common-edge bumped from 2.2.1 to 2.2.2 +## [1.3.28](https://github.com/launchdarkly/js-core/compare/vercel-server-sdk-v1.3.27...vercel-server-sdk-v1.3.28) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common-edge bumped from 2.6.3 to 2.6.4 + ## [1.3.27](https://github.com/launchdarkly/js-core/compare/vercel-server-sdk-v1.3.26...vercel-server-sdk-v1.3.27) (2025-04-08) diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index 399e11f696..c50de76763 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/vercel-server-sdk", - "version": "1.3.27", + "version": "1.3.28", "description": "LaunchDarkly Server-Side SDK for Vercel Edge", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/vercel", "repository": { @@ -36,7 +36,7 @@ "check": "yarn prettier && yarn lint && yarn build && yarn test" }, "dependencies": { - "@launchdarkly/js-server-sdk-common-edge": "2.6.3", + "@launchdarkly/js-server-sdk-common-edge": "2.6.4", "@vercel/edge-config": "^1.1.0", "crypto-js": "^4.1.1" }, diff --git a/packages/shared/akamai-edgeworker-sdk/CHANGELOG.md b/packages/shared/akamai-edgeworker-sdk/CHANGELOG.md index 133d485bb2..2ac65acc3a 100644 --- a/packages/shared/akamai-edgeworker-sdk/CHANGELOG.md +++ b/packages/shared/akamai-edgeworker-sdk/CHANGELOG.md @@ -86,6 +86,15 @@ All notable changes to the LaunchDarkly SDK for Akamai Workers will be documente * dependencies * @launchdarkly/js-server-sdk-common bumped from ^2.2.1 to ^2.2.2 +## [2.0.5](https://github.com/launchdarkly/js-core/compare/akamai-edgeworker-sdk-common-v2.0.4...akamai-edgeworker-sdk-common-v2.0.5) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from ^2.14.0 to ^2.15.0 + ## [2.0.4](https://github.com/launchdarkly/js-core/compare/akamai-edgeworker-sdk-common-v2.0.3...akamai-edgeworker-sdk-common-v2.0.4) (2025-04-08) diff --git a/packages/shared/akamai-edgeworker-sdk/package.json b/packages/shared/akamai-edgeworker-sdk/package.json index 95ee5386f2..2e38c931c1 100644 --- a/packages/shared/akamai-edgeworker-sdk/package.json +++ b/packages/shared/akamai-edgeworker-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/akamai-edgeworker-sdk-common", - "version": "2.0.4", + "version": "2.0.5", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/shared/akamai-edge-sdk", "repository": { "type": "git", @@ -55,7 +55,7 @@ "typescript": "5.1.6" }, "dependencies": { - "@launchdarkly/js-server-sdk-common": "^2.14.0", + "@launchdarkly/js-server-sdk-common": "^2.15.0", "crypto-js": "^4.1.1" } } diff --git a/packages/shared/common/CHANGELOG.md b/packages/shared/common/CHANGELOG.md index 412629f454..90557ef65f 100644 --- a/packages/shared/common/CHANGELOG.md +++ b/packages/shared/common/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to `@launchdarkly/js-sdk-common` will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.16.0](https://github.com/launchdarkly/js-core/compare/js-sdk-common-v2.15.0...js-sdk-common-v2.16.0) (2025-04-16) + + +### Features + +* Environment ID support for hooks ([#823](https://github.com/launchdarkly/js-core/issues/823)) ([63dc9f9](https://github.com/launchdarkly/js-core/commit/63dc9f9f1300c598e79be27909f8195ac66d54ef)) + ## [2.15.0](https://github.com/launchdarkly/js-core/compare/js-sdk-common-v2.14.0...js-sdk-common-v2.15.0) (2025-04-08) diff --git a/packages/shared/common/__tests__/internal/metadata/InitMetadata.test.ts b/packages/shared/common/__tests__/internal/metadata/InitMetadata.test.ts new file mode 100644 index 0000000000..5d6d132ba4 --- /dev/null +++ b/packages/shared/common/__tests__/internal/metadata/InitMetadata.test.ts @@ -0,0 +1,17 @@ +import { initMetadataFromHeaders } from '../../../src/internal/metadata'; + +it('handles passing undefined headers', () => { + expect(initMetadataFromHeaders()).toBeUndefined(); +}); + +it('handles missing x-ld-envid header', () => { + expect(initMetadataFromHeaders({})).toBeUndefined(); +}); + +it('retrieves environmentId from headers', () => { + expect(initMetadataFromHeaders({ 'x-ld-envid': '12345' })).toEqual({ environmentId: '12345' }); +}); + +it('retrieves environmentId from mixed case header', () => { + expect(initMetadataFromHeaders({ 'X-LD-EnvId': '12345' })).toEqual({ environmentId: '12345' }); +}); diff --git a/packages/shared/common/package.json b/packages/shared/common/package.json index e4729897d4..7285f611ec 100644 --- a/packages/shared/common/package.json +++ b/packages/shared/common/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/js-sdk-common", - "version": "2.15.0", + "version": "2.16.0", "type": "module", "main": "./dist/esm/index.mjs", "types": "./dist/esm/index.d.ts", diff --git a/packages/shared/common/src/api/platform/EventSource.ts b/packages/shared/common/src/api/platform/EventSource.ts index f44fc830b2..3c0d920700 100644 --- a/packages/shared/common/src/api/platform/EventSource.ts +++ b/packages/shared/common/src/api/platform/EventSource.ts @@ -4,13 +4,13 @@ export type EventName = string; export type EventListener = (event?: { data?: any }) => void; export type ProcessStreamResponse = { deserializeData: (data: string) => any; - processJson: (json: any) => void; + processJson: (json: any, initHeaders?: { [key: string]: string }) => void; }; export interface EventSource { onclose: (() => void) | undefined; onerror: ((err?: HttpErrorResponse) => void) | undefined; - onopen: (() => void) | undefined; + onopen: ((e: { headers?: { [key: string]: string } }) => void) | undefined; onretrying: ((e: { delayMillis: number }) => void) | undefined; addEventListener(type: EventName, listener: EventListener): void; diff --git a/packages/shared/common/src/internal/index.ts b/packages/shared/common/src/internal/index.ts index 282da8f91f..ae6f5cb844 100644 --- a/packages/shared/common/src/internal/index.ts +++ b/packages/shared/common/src/internal/index.ts @@ -3,3 +3,4 @@ export * from './diagnostics'; export * from './evaluation'; export * from './events'; export * from './fdv2'; +export * from './metadata'; diff --git a/packages/shared/common/src/internal/metadata/InitMetadata.ts b/packages/shared/common/src/internal/metadata/InitMetadata.ts new file mode 100644 index 0000000000..db67fa3aef --- /dev/null +++ b/packages/shared/common/src/internal/metadata/InitMetadata.ts @@ -0,0 +1,26 @@ +/** + * Metadata used to initialize an LDFeatureStore. + */ +export interface InitMetadata { + environmentId: string; +} + +/** + * Creates an InitMetadata object from initialization headers. + * + * @param initHeaders Initialization headers received when establishing + * a streaming or polling connection to LD. + * @returns InitMetadata object, or undefined if initHeaders is undefined + * or missing the required header values. + */ +export function initMetadataFromHeaders(initHeaders?: { + [key: string]: string; +}): InitMetadata | undefined { + if (initHeaders) { + const envIdKey = Object.keys(initHeaders).find((key) => key.toLowerCase() === 'x-ld-envid'); + if (envIdKey) { + return { environmentId: initHeaders[envIdKey] }; + } + } + return undefined; +} diff --git a/packages/shared/common/src/internal/metadata/index.ts b/packages/shared/common/src/internal/metadata/index.ts new file mode 100644 index 0000000000..7e96b4a998 --- /dev/null +++ b/packages/shared/common/src/internal/metadata/index.ts @@ -0,0 +1,3 @@ +import { InitMetadata, initMetadataFromHeaders } from './InitMetadata'; + +export { InitMetadata, initMetadataFromHeaders }; diff --git a/packages/shared/sdk-client/CHANGELOG.md b/packages/shared/sdk-client/CHANGELOG.md index 7150fb8593..5cdc76f76b 100644 --- a/packages/shared/sdk-client/CHANGELOG.md +++ b/packages/shared/sdk-client/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.12.6](https://github.com/launchdarkly/js-core/compare/js-client-sdk-common-v1.12.5...js-client-sdk-common-v1.12.6) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-sdk-common bumped from 2.15.0 to 2.16.0 + ## [1.12.5](https://github.com/launchdarkly/js-core/compare/js-client-sdk-common-v1.12.4...js-client-sdk-common-v1.12.5) (2025-04-08) diff --git a/packages/shared/sdk-client/package.json b/packages/shared/sdk-client/package.json index 580a49d452..8100faf03b 100644 --- a/packages/shared/sdk-client/package.json +++ b/packages/shared/sdk-client/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/js-client-sdk-common", - "version": "1.12.5", + "version": "1.12.6", "type": "module", "main": "./dist/esm/index.mjs", "types": "./dist/esm/index.d.ts", @@ -43,7 +43,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@launchdarkly/js-sdk-common": "2.15.0" + "@launchdarkly/js-sdk-common": "2.16.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.0", diff --git a/packages/shared/sdk-server-edge/CHANGELOG.md b/packages/shared/sdk-server-edge/CHANGELOG.md index 68b3f4837c..473b48bfee 100644 --- a/packages/shared/sdk-server-edge/CHANGELOG.md +++ b/packages/shared/sdk-server-edge/CHANGELOG.md @@ -96,6 +96,15 @@ * dependencies * @launchdarkly/js-server-sdk-common bumped from 2.2.1 to 2.2.2 +## [2.6.4](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-edge-v2.6.3...js-server-sdk-common-edge-v2.6.4) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from 2.14.0 to 2.15.0 + ## [2.6.3](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-edge-v2.6.2...js-server-sdk-common-edge-v2.6.3) (2025-04-08) diff --git a/packages/shared/sdk-server-edge/package.json b/packages/shared/sdk-server-edge/package.json index d598890649..1c82f851ea 100644 --- a/packages/shared/sdk-server-edge/package.json +++ b/packages/shared/sdk-server-edge/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/js-server-sdk-common-edge", - "version": "2.6.3", + "version": "2.6.4", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/shared/sdk-server-edge", "repository": { "type": "git", @@ -36,7 +36,7 @@ "check": "yarn prettier && yarn lint && yarn build && yarn test && yarn doc" }, "dependencies": { - "@launchdarkly/js-server-sdk-common": "2.14.0", + "@launchdarkly/js-server-sdk-common": "2.15.0", "crypto-js": "^4.1.1" }, "devDependencies": { diff --git a/packages/shared/sdk-server/CHANGELOG.md b/packages/shared/sdk-server/CHANGELOG.md index 31f78f2110..bf701c3f4d 100644 --- a/packages/shared/sdk-server/CHANGELOG.md +++ b/packages/shared/sdk-server/CHANGELOG.md @@ -8,6 +8,20 @@ All notable changes to `@launchdarkly/js-server-sdk-common` will be documented i * dependencies * @launchdarkly/js-sdk-common bumped from 2.3.0 to 2.3.1 +## [2.15.0](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-v2.14.0...js-server-sdk-common-v2.15.0) (2025-04-16) + + +### Features + +* Environment ID support for hooks ([#823](https://github.com/launchdarkly/js-core/issues/823)) ([63dc9f9](https://github.com/launchdarkly/js-core/commit/63dc9f9f1300c598e79be27909f8195ac66d54ef)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-sdk-common bumped from 2.15.0 to 2.16.0 + ## [2.14.0](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-v2.13.0...js-server-sdk-common-v2.14.0) (2025-04-08) diff --git a/packages/shared/sdk-server/__tests__/data_sources/DataSourceUpdates.test.ts b/packages/shared/sdk-server/__tests__/data_sources/DataSourceUpdates.test.ts index ff474c5f77..401589febd 100644 --- a/packages/shared/sdk-server/__tests__/data_sources/DataSourceUpdates.test.ts +++ b/packages/shared/sdk-server/__tests__/data_sources/DataSourceUpdates.test.ts @@ -1,11 +1,29 @@ import { AsyncQueue } from 'launchdarkly-js-test-helpers'; +import { internal } from '@launchdarkly/js-sdk-common'; + import { LDFeatureStore } from '../../src/api/subsystems'; import promisify from '../../src/async/promisify'; import DataSourceUpdates from '../../src/data_sources/DataSourceUpdates'; import InMemoryFeatureStore from '../../src/store/InMemoryFeatureStore'; import VersionedDataKinds from '../../src/store/VersionedDataKinds'; +type InitMetadata = internal.InitMetadata; + +it('passes initialization metadata to underlying feature store', () => { + const metadata: InitMetadata = { environmentId: '12345' }; + const store = new InMemoryFeatureStore(); + store.init = jest.fn(); + const updates = new DataSourceUpdates( + store, + () => false, + () => {}, + ); + updates.init({}, () => {}, metadata); + expect(store.init).toHaveBeenCalledTimes(1); + expect(store.init).toHaveBeenNthCalledWith(1, expect.any(Object), expect.any(Function), metadata); +}); + describe.each([true, false])( 'given a DataSourceUpdates with in memory store and change listeners: %s', (listen) => { diff --git a/packages/shared/sdk-server/__tests__/data_sources/PollingProcessor.test.ts b/packages/shared/sdk-server/__tests__/data_sources/PollingProcessor.test.ts index 05ae9ff282..1e43688301 100644 --- a/packages/shared/sdk-server/__tests__/data_sources/PollingProcessor.test.ts +++ b/packages/shared/sdk-server/__tests__/data_sources/PollingProcessor.test.ts @@ -76,6 +76,18 @@ describe('given an event processor', () => { expect(flags).toEqual(allData.flags); expect(segments).toEqual(allData.segments); }); + + it('initializes the feature store with metadata', () => { + const initHeaders = { + 'x-ld-envid': '12345', + }; + requestor.requestAllData = jest.fn((cb) => cb(undefined, jsonData, initHeaders)); + + processor.start(); + const metadata = storeFacade.getInitMetadata?.(); + + expect(metadata).toEqual({ environmentId: '12345' }); + }); }); describe('given a polling processor with a short poll duration', () => { diff --git a/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts b/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts index 3f3d8537a2..54b68f6312 100644 --- a/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts +++ b/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts @@ -49,7 +49,7 @@ describe('given a requestor', () => { throw new Error('Function not implemented.'); }, entries(): Iterable<[string, string]> { - throw new Error('Function not implemented.'); + return testHeaders ? Object.entries(testHeaders) : []; }, has(_name: string): boolean { throw new Error('Function not implemented.'); @@ -115,7 +115,9 @@ describe('given a requestor', () => { }); it('stores and sends etags', async () => { - testHeaders.etag = 'abc123'; + testHeaders = { + etag: 'abc123', + }; testResponse = 'a response'; const res1 = await promisify<{ err: any; body: any }>((cb) => { requestor.requestAllData((err, body) => cb({ err, body })); @@ -134,4 +136,17 @@ describe('given a requestor', () => { expect(req1.options.headers?.['if-none-match']).toBe(undefined); expect(req2.options.headers?.['if-none-match']).toBe((testHeaders.etag = 'abc123')); }); + + it('passes response headers to callback', async () => { + testHeaders = { + header1: 'value1', + header2: 'value2', + header3: 'value3', + }; + const res = await promisify<{ err: any; body: any; headers: any }>((cb) => { + requestor.requestAllData((err, body, headers) => cb({ err, body, headers })); + }); + + expect(res.headers).toEqual(testHeaders); + }); }); diff --git a/packages/shared/sdk-server/__tests__/data_sources/StreamingProcessor.test.ts b/packages/shared/sdk-server/__tests__/data_sources/StreamingProcessor.test.ts index f2b7b21aad..a91a90ffd2 100644 --- a/packages/shared/sdk-server/__tests__/data_sources/StreamingProcessor.test.ts +++ b/packages/shared/sdk-server/__tests__/data_sources/StreamingProcessor.test.ts @@ -138,7 +138,7 @@ describe('given a stream processor with mock event source', () => { }); it('uses expected uri and eventSource init args', () => { - expect(basicPlatform.requests.createEventSource).toBeCalledWith( + expect(basicPlatform.requests.createEventSource).toHaveBeenCalledWith( `${serviceEndpoints.streaming}/all`, { errorFilter: expect.any(Function), @@ -200,32 +200,44 @@ describe('given a stream processor with mock event source', () => { const patchHandler = mockEventSource.addEventListener.mock.calls[1][1]; patchHandler(event); - expect(mockListener.deserializeData).toBeCalledTimes(2); - expect(mockListener.processJson).toBeCalledTimes(2); + expect(mockListener.deserializeData).toHaveBeenCalledTimes(2); + expect(mockListener.processJson).toHaveBeenCalledTimes(2); + }); + + it('passes initialization headers to listener', () => { + const headers = { + header1: 'value1', + header2: 'value2', + header3: 'value3', + }; + mockEventSource.onopen({ type: 'open', headers }); + simulatePutEvent(); + expect(mockListener.processJson).toHaveBeenCalledTimes(1); + expect(mockListener.processJson).toHaveBeenNthCalledWith(1, expect.any(Object), headers); }); it('passes error to callback if json data is malformed', async () => { (mockListener.deserializeData as jest.Mock).mockReturnValue(false); simulatePutEvent(); - expect(logger.error).toBeCalledWith(expect.stringMatching(/invalid data in "put"/)); - expect(logger.debug).toBeCalledWith(expect.stringMatching(/invalid json/i)); + expect(logger.error).toHaveBeenCalledWith(expect.stringMatching(/invalid data in "put"/)); + expect(logger.debug).toHaveBeenCalledWith(expect.stringMatching(/invalid json/i)); expect(mockErrorHandler.mock.lastCall[0].message).toMatch(/malformed json/i); }); it('calls error handler if event.data prop is missing', async () => { simulatePutEvent({ flags: {} }); - expect(mockListener.deserializeData).not.toBeCalled(); - expect(mockListener.processJson).not.toBeCalled(); + expect(mockListener.deserializeData).not.toHaveBeenCalled(); + expect(mockListener.processJson).not.toHaveBeenCalled(); expect(mockErrorHandler.mock.lastCall[0].message).toMatch(/unexpected payload/i); }); it('closes and stops', async () => { streamingProcessor.close(); - expect(streamingProcessor.stop).toBeCalled(); - expect(mockEventSource.close).toBeCalled(); + expect(streamingProcessor.stop).toHaveBeenCalled(); + expect(mockEventSource.close).toHaveBeenCalled(); // @ts-ignore expect(streamingProcessor.eventSource).toBeUndefined(); }); @@ -249,8 +261,8 @@ describe('given a stream processor with mock event source', () => { const willRetry = simulateError(testError); expect(willRetry).toBeTruthy(); - expect(mockErrorHandler).not.toBeCalled(); - expect(logger.warn).toBeCalledWith( + expect(mockErrorHandler).not.toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalledWith( expect.stringMatching(new RegExp(`${status}.*will retry`)), ); @@ -270,10 +282,10 @@ describe('given a stream processor with mock event source', () => { const willRetry = simulateError(testError); expect(willRetry).toBeFalsy(); - expect(mockErrorHandler).toBeCalledWith( + expect(mockErrorHandler).toHaveBeenCalledWith( new LDStreamingError(DataSourceErrorKind.Unknown, testError.message, testError.status), ); - expect(logger.error).toBeCalledWith( + expect(logger.error).toHaveBeenCalledWith( expect.stringMatching(new RegExp(`${status}.*permanently`)), ); diff --git a/packages/shared/sdk-server/__tests__/data_sources/createStreamListeners.test.ts b/packages/shared/sdk-server/__tests__/data_sources/createStreamListeners.test.ts index 3237b9417c..782ce51837 100644 --- a/packages/shared/sdk-server/__tests__/data_sources/createStreamListeners.test.ts +++ b/packages/shared/sdk-server/__tests__/data_sources/createStreamListeners.test.ts @@ -94,13 +94,36 @@ describe('createStreamListeners', () => { processJson(allData); - expect(logger.debug).toBeCalledWith(expect.stringMatching(/initializing/i)); - expect(dataSourceUpdates.init).toBeCalledWith( + expect(logger.debug).toHaveBeenCalledWith(expect.stringMatching(/initializing/i)); + expect(dataSourceUpdates.init).toHaveBeenCalledWith( { features: flags, segments, }, onPutCompleteHandler, + undefined, + ); + }); + + test('data source init is called with initialization metadata', async () => { + const listeners = createStreamListeners(dataSourceUpdates, logger, onCompleteHandlers); + const { processJson } = listeners.get('put')!; + const { + data: { flags, segments }, + } = allData; + const initHeaders = { + 'x-ld-envid': '12345', + }; + processJson(allData, initHeaders); + + expect(logger.debug).toHaveBeenCalledWith(expect.stringMatching(/initializing/i)); + expect(dataSourceUpdates.init).toHaveBeenCalledWith( + { + features: flags, + segments, + }, + onPutCompleteHandler, + { environmentId: '12345' }, ); }); }); @@ -121,8 +144,8 @@ describe('createStreamListeners', () => { processJson(patchData); - expect(logger.debug).toBeCalledWith(expect.stringMatching(/updating/i)); - expect(dataSourceUpdates.upsert).toBeCalledWith(kind, data, onPatchCompleteHandler); + expect(logger.debug).toHaveBeenCalledWith(expect.stringMatching(/updating/i)); + expect(dataSourceUpdates.upsert).toHaveBeenCalledWith(kind, data, onPatchCompleteHandler); }); test('data source upsert not called missing kind', async () => { @@ -132,7 +155,7 @@ describe('createStreamListeners', () => { processJson(missingKind); - expect(dataSourceUpdates.upsert).not.toBeCalled(); + expect(dataSourceUpdates.upsert).not.toHaveBeenCalled(); }); test('data source upsert not called wrong namespace path', async () => { @@ -142,7 +165,7 @@ describe('createStreamListeners', () => { processJson(wrongKey); - expect(dataSourceUpdates.upsert).not.toBeCalled(); + expect(dataSourceUpdates.upsert).not.toHaveBeenCalled(); }); }); @@ -162,8 +185,8 @@ describe('createStreamListeners', () => { processJson(deleteData); - expect(logger.debug).toBeCalledWith(expect.stringMatching(/deleting/i)); - expect(dataSourceUpdates.upsert).toBeCalledWith( + expect(logger.debug).toHaveBeenCalledWith(expect.stringMatching(/deleting/i)); + expect(dataSourceUpdates.upsert).toHaveBeenCalledWith( kind, { key: 'flagkey', version, deleted: true }, onDeleteCompleteHandler, @@ -177,7 +200,7 @@ describe('createStreamListeners', () => { processJson(missingKind); - expect(dataSourceUpdates.upsert).not.toBeCalled(); + expect(dataSourceUpdates.upsert).not.toHaveBeenCalled(); }); test('data source upsert not called wrong namespace path', async () => { @@ -187,7 +210,7 @@ describe('createStreamListeners', () => { processJson(wrongKey); - expect(dataSourceUpdates.upsert).not.toBeCalled(); + expect(dataSourceUpdates.upsert).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/shared/sdk-server/__tests__/hooks/HookRunner.test.ts b/packages/shared/sdk-server/__tests__/hooks/HookRunner.test.ts index b72a184f87..cd97d3fe38 100644 --- a/packages/shared/sdk-server/__tests__/hooks/HookRunner.test.ts +++ b/packages/shared/sdk-server/__tests__/hooks/HookRunner.test.ts @@ -36,6 +36,7 @@ describe('given a HookRunner', () => { reason: { kind: 'ERROR', errorKind: 'FLAG_NOT_FOUND' }, variationIndex: null, }), + '12345', ); testHook.verifyAfter( @@ -44,6 +45,7 @@ describe('given a HookRunner', () => { context: { ...defaultUser }, defaultValue: false, method: 'LDClient.variation', + environmentId: '12345', }, { added: 'added data' }, { @@ -187,6 +189,7 @@ it('can add a hook after initialization', async () => { reason: { kind: 'FALLTHROUGH' }, variationIndex: 0, }), + '12345', ); testHook.verifyBefore( { @@ -194,6 +197,7 @@ it('can add a hook after initialization', async () => { context: { ...defaultUser }, defaultValue: false, method: 'LDClient.variation', + environmentId: '12345', }, {}, ); @@ -203,6 +207,7 @@ it('can add a hook after initialization', async () => { context: { ...defaultUser }, defaultValue: false, method: 'LDClient.variation', + environmentId: '12345', }, {}, { diff --git a/packages/shared/sdk-server/__tests__/store/InMemoryFeatureStore.test.ts b/packages/shared/sdk-server/__tests__/store/InMemoryFeatureStore.test.ts index 0f77700a2f..114db7e849 100644 --- a/packages/shared/sdk-server/__tests__/store/InMemoryFeatureStore.test.ts +++ b/packages/shared/sdk-server/__tests__/store/InMemoryFeatureStore.test.ts @@ -147,4 +147,21 @@ describe('given an initialized feature store', () => { const feature = await featureStore.get({ namespace: 'potato' }, newPotato.key); expect(feature).toEqual(newPotato); }); + + it('returns undefined initMetadata', () => { + expect(featureStore.getInitMetadata?.()).toBeUndefined(); + }); +}); + +describe('given an initialized feature store with metadata', () => { + let featureStore: AsyncStoreFacade; + + beforeEach(async () => { + featureStore = new AsyncStoreFacade(new InMemoryFeatureStore()); + await featureStore.init({}, { environmentId: '12345' }); + }); + + it('returns correct metadata', () => { + expect(featureStore.getInitMetadata?.()).toEqual({ environmentId: '12345' }); + }); }); diff --git a/packages/shared/sdk-server/package.json b/packages/shared/sdk-server/package.json index cba0896304..cbc3de57b0 100644 --- a/packages/shared/sdk-server/package.json +++ b/packages/shared/sdk-server/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/js-server-sdk-common", - "version": "2.14.0", + "version": "2.15.0", "type": "commonjs", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,7 +27,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@launchdarkly/js-sdk-common": "2.15.0", + "@launchdarkly/js-sdk-common": "2.16.0", "semver": "7.5.4" }, "devDependencies": { diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 152b7c066c..0b1d79901e 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -344,6 +344,7 @@ export default class LDClientImpl implements LDClient { }, ); }), + this._featureStore.getInitMetaData?.()?.environmentId, ) .then((detail) => { callback?.(null, detail.value); @@ -375,6 +376,7 @@ export default class LDClientImpl implements LDClient { }, ); }), + this._featureStore.getInitMetaData?.()?.environmentId, ); } @@ -409,6 +411,7 @@ export default class LDClientImpl implements LDClient { typeChecker, ); }), + this._featureStore.getInitMetaData?.()?.environmentId, ); } @@ -470,6 +473,7 @@ export default class LDClientImpl implements LDClient { }, ); }), + this._featureStore.getInitMetaData?.()?.environmentId, ) .then((detail) => detail.value); } @@ -541,6 +545,7 @@ export default class LDClientImpl implements LDClient { }, ); }), + this._featureStore.getInitMetaData?.()?.environmentId, ); } @@ -615,6 +620,7 @@ export default class LDClientImpl implements LDClient { defaultValue, MIGRATION_VARIATION_METHOD_NAME, () => this._migrationVariationInternal(key, context, defaultValue), + this._featureStore.getInitMetaData?.()?.environmentId, ); return res.migration; diff --git a/packages/shared/sdk-server/src/api/integrations/Hook.ts b/packages/shared/sdk-server/src/api/integrations/Hook.ts index 52e7639866..71d023a5ad 100644 --- a/packages/shared/sdk-server/src/api/integrations/Hook.ts +++ b/packages/shared/sdk-server/src/api/integrations/Hook.ts @@ -8,6 +8,7 @@ export interface EvaluationSeriesContext { readonly context: LDContext; readonly defaultValue: unknown; readonly method: string; + readonly environmentId?: string; } /** diff --git a/packages/shared/sdk-server/src/api/subsystems/LDDataSourceUpdates.ts b/packages/shared/sdk-server/src/api/subsystems/LDDataSourceUpdates.ts index 8b3badd386..4941d0c5b5 100644 --- a/packages/shared/sdk-server/src/api/subsystems/LDDataSourceUpdates.ts +++ b/packages/shared/sdk-server/src/api/subsystems/LDDataSourceUpdates.ts @@ -1,6 +1,10 @@ +import { internal } from '@launchdarkly/js-sdk-common'; + import { DataKind } from '../interfaces'; import { LDFeatureStoreDataStorage, LDKeyedFeatureStoreItem } from './LDFeatureStore'; +type InitMetadata = internal.InitMetadata; + /** * Interface that a data source implementation will use to push data into the SDK. * @@ -19,8 +23,11 @@ export interface LDDataSourceUpdates { * * @param callback * Will be called when the store has been initialized. + * + * @param initMetadata + * Optional metadata to initialize the data source with. */ - init(allData: LDFeatureStoreDataStorage, callback: () => void): void; + init(allData: LDFeatureStoreDataStorage, callback: () => void, initMetadata?: InitMetadata): void; /** * Updates or inserts an item in the specified collection. For updates, the object will only be diff --git a/packages/shared/sdk-server/src/api/subsystems/LDFeatureRequestor.ts b/packages/shared/sdk-server/src/api/subsystems/LDFeatureRequestor.ts index c287d6617a..43b3c261c2 100644 --- a/packages/shared/sdk-server/src/api/subsystems/LDFeatureRequestor.ts +++ b/packages/shared/sdk-server/src/api/subsystems/LDFeatureRequestor.ts @@ -6,5 +6,5 @@ * @ignore */ export interface LDFeatureRequestor { - requestAllData: (cb: (err: any, body: any) => void) => void; + requestAllData: (cb: (err: any, body: any, headers: any) => void) => void; } diff --git a/packages/shared/sdk-server/src/api/subsystems/LDFeatureStore.ts b/packages/shared/sdk-server/src/api/subsystems/LDFeatureStore.ts index 9bdfb94307..ef3f8cac2d 100644 --- a/packages/shared/sdk-server/src/api/subsystems/LDFeatureStore.ts +++ b/packages/shared/sdk-server/src/api/subsystems/LDFeatureStore.ts @@ -1,5 +1,9 @@ +import { internal } from '@launchdarkly/js-sdk-common'; + import { DataKind } from '../interfaces'; +type InitMetadata = internal.InitMetadata; + /** * Represents an item which can be stored in the feature store. */ @@ -92,8 +96,11 @@ export interface LDFeatureStore { * * @param callback * Will be called when the store has been initialized. + * + * @param initMetadata + * Optional metadata to initialize the feature store with. */ - init(allData: LDFeatureStoreDataStorage, callback: () => void): void; + init(allData: LDFeatureStoreDataStorage, callback: () => void, initMetadata?: InitMetadata): void; /** * Delete an entity from the store. @@ -158,4 +165,9 @@ export interface LDFeatureStore { * Get a description of the store. */ getDescription?(): string; + + /** + * Get the initialization metadata of the store. + */ + getInitMetaData?(): InitMetadata | undefined; } diff --git a/packages/shared/sdk-server/src/data_sources/DataSourceUpdates.ts b/packages/shared/sdk-server/src/data_sources/DataSourceUpdates.ts index ac6e3820dc..e1c190a531 100644 --- a/packages/shared/sdk-server/src/data_sources/DataSourceUpdates.ts +++ b/packages/shared/sdk-server/src/data_sources/DataSourceUpdates.ts @@ -1,3 +1,5 @@ +import { internal } from '@launchdarkly/js-sdk-common'; + import { DataKind } from '../api/interfaces'; import { LDDataSourceUpdates, @@ -13,6 +15,8 @@ import VersionedDataKinds from '../store/VersionedDataKinds'; import DependencyTracker from './DependencyTracker'; import NamespacedDataSet from './NamespacedDataSet'; +type InitMetadata = internal.InitMetadata; + /** * This type allows computing the clause dependencies of either a flag or a segment. */ @@ -66,46 +70,54 @@ export default class DataSourceUpdates implements LDDataSourceUpdates { private readonly _onChange: (key: string) => void, ) {} - init(allData: LDFeatureStoreDataStorage, callback: () => void): void { + init( + allData: LDFeatureStoreDataStorage, + callback: () => void, + initMetadata?: InitMetadata, + ): void { const checkForChanges = this._hasEventListeners(); const doInit = (oldData?: LDFeatureStoreDataStorage) => { - this._featureStore.init(allData, () => { - // Defer change events so they execute after the callback. - Promise.resolve().then(() => { - this._dependencyTracker.reset(); - - Object.entries(allData).forEach(([namespace, items]) => { - Object.keys(items || {}).forEach((key) => { - const item = items[key]; - this._dependencyTracker.updateDependenciesFrom( - namespace, - key, - computeDependencies(namespace, item), - ); - }); - }); + this._featureStore.init( + allData, + () => { + // Defer change events so they execute after the callback. + Promise.resolve().then(() => { + this._dependencyTracker.reset(); - if (checkForChanges) { - const updatedItems = new NamespacedDataSet(); - Object.keys(allData).forEach((namespace) => { - const oldDataForKind = oldData?.[namespace] || {}; - const newDataForKind = allData[namespace]; - const mergedData = { ...oldDataForKind, ...newDataForKind }; - Object.keys(mergedData).forEach((key) => { - this.addIfModified( + Object.entries(allData).forEach(([namespace, items]) => { + Object.keys(items || {}).forEach((key) => { + const item = items[key]; + this._dependencyTracker.updateDependenciesFrom( namespace, key, - oldDataForKind && oldDataForKind[key], - newDataForKind && newDataForKind[key], - updatedItems, + computeDependencies(namespace, item), ); }); }); - this.sendChangeEvents(updatedItems); - } - }); - callback?.(); - }); + + if (checkForChanges) { + const updatedItems = new NamespacedDataSet(); + Object.keys(allData).forEach((namespace) => { + const oldDataForKind = oldData?.[namespace] || {}; + const newDataForKind = allData[namespace]; + const mergedData = { ...oldDataForKind, ...newDataForKind }; + Object.keys(mergedData).forEach((key) => { + this.addIfModified( + namespace, + key, + oldDataForKind && oldDataForKind[key], + newDataForKind && newDataForKind[key], + updatedItems, + ); + }); + }); + this.sendChangeEvents(updatedItems); + } + }); + callback?.(); + }, + initMetadata, + ); }; if (checkForChanges) { diff --git a/packages/shared/sdk-server/src/data_sources/PollingProcessor.ts b/packages/shared/sdk-server/src/data_sources/PollingProcessor.ts index d376b45354..07ef5bf9f0 100644 --- a/packages/shared/sdk-server/src/data_sources/PollingProcessor.ts +++ b/packages/shared/sdk-server/src/data_sources/PollingProcessor.ts @@ -1,6 +1,7 @@ import { DataSourceErrorKind, httpErrorMessage, + internal, isHttpRecoverable, LDLogger, LDPollingError, @@ -16,6 +17,8 @@ import Requestor from './Requestor'; export type PollingErrorHandler = (err: LDPollingError) => void; +const { initMetadataFromHeaders } = internal; + /** * @internal */ @@ -57,7 +60,7 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { const startTime = Date.now(); this._logger?.debug('Polling LaunchDarkly for feature flag updates'); - this._requestor.requestAllData((err, body) => { + this._requestor.requestAllData((err, body, headers) => { const elapsed = Date.now() - startTime; const sleepFor = Math.max(this._pollInterval * 1000 - elapsed, 0); @@ -86,13 +89,17 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { [VersionedDataKinds.Features.namespace]: parsed.flags, [VersionedDataKinds.Segments.namespace]: parsed.segments, }; - this._featureStore.init(initData, () => { - this._initSuccessHandler(); - // Triggering the next poll after the init has completed. - this._timeoutHandle = setTimeout(() => { - this._poll(); - }, sleepFor); - }); + this._featureStore.init( + initData, + () => { + this._initSuccessHandler(); + // Triggering the next poll after the init has completed. + this._timeoutHandle = setTimeout(() => { + this._poll(); + }, sleepFor); + }, + initMetadataFromHeaders(headers), + ); // The poll will be triggered by the feature store initialization // completing. return; diff --git a/packages/shared/sdk-server/src/data_sources/Requestor.ts b/packages/shared/sdk-server/src/data_sources/Requestor.ts index 0d3567eae8..4f59bda9b7 100644 --- a/packages/shared/sdk-server/src/data_sources/Requestor.ts +++ b/packages/shared/sdk-server/src/data_sources/Requestor.ts @@ -70,7 +70,7 @@ export default class Requestor implements LDFeatureRequestor { return { res, body }; } - async requestAllData(cb: (err: any, body: any) => void) { + async requestAllData(cb: (err: any, body: any, headers: any) => void) { const options: Options = { method: 'GET', headers: this._headers, @@ -83,11 +83,15 @@ export default class Requestor implements LDFeatureRequestor { `Unexpected status code: ${res.status}`, res.status, ); - return cb(err, undefined); + return cb(err, undefined, undefined); } - return cb(undefined, res.status === 304 ? null : body); + return cb( + undefined, + res.status === 304 ? null : body, + Object.fromEntries(res.headers.entries()), + ); } catch (err) { - return cb(err, undefined); + return cb(err, undefined, undefined); } } } diff --git a/packages/shared/sdk-server/src/data_sources/StreamingProcessor.ts b/packages/shared/sdk-server/src/data_sources/StreamingProcessor.ts index e752f4863c..41cd409174 100644 --- a/packages/shared/sdk-server/src/data_sources/StreamingProcessor.ts +++ b/packages/shared/sdk-server/src/data_sources/StreamingProcessor.ts @@ -38,6 +38,7 @@ export default class StreamingProcessor implements subsystem.LDStreamProcessor { private _eventSource?: EventSource; private _requests: Requests; private _connectionAttemptStartTime?: number; + private _initHeaders?: { [key: string]: string }; constructor( clientContext: ClientContext, @@ -125,7 +126,8 @@ export default class StreamingProcessor implements subsystem.LDStreamProcessor { // The work is done by `errorFilter`. }; - eventSource.onopen = () => { + eventSource.onopen = (e) => { + this._initHeaders = e.headers; this._logger?.info('Opened LaunchDarkly stream connection'); }; @@ -146,7 +148,7 @@ export default class StreamingProcessor implements subsystem.LDStreamProcessor { reportJsonError(eventName, data, this._logger, this._errorHandler); return; } - processJson(dataJson); + processJson(dataJson, this._initHeaders); } else { this._errorHandler?.( new LDStreamingError( diff --git a/packages/shared/sdk-server/src/data_sources/createStreamListeners.ts b/packages/shared/sdk-server/src/data_sources/createStreamListeners.ts index 391e141914..6e453196cb 100644 --- a/packages/shared/sdk-server/src/data_sources/createStreamListeners.ts +++ b/packages/shared/sdk-server/src/data_sources/createStreamListeners.ts @@ -1,5 +1,6 @@ import { EventName, + internal, LDLogger, ProcessStreamResponse, VoidFunction, @@ -16,20 +17,24 @@ import { } from '../store/serialization'; import VersionedDataKinds from '../store/VersionedDataKinds'; +const { initMetadataFromHeaders } = internal; + export const createPutListener = ( dataSourceUpdates: LDDataSourceUpdates, logger?: LDLogger, onPutCompleteHandler: VoidFunction = () => {}, ) => ({ deserializeData: deserializeAll, - processJson: async ({ data: { flags, segments } }: AllData) => { + processJson: async ( + { data: { flags, segments } }: AllData, + initHeaders?: { [key: string]: string }, + ) => { const initData = { [VersionedDataKinds.Features.namespace]: flags, [VersionedDataKinds.Segments.namespace]: segments, }; - logger?.debug('Initializing all data'); - dataSourceUpdates.init(initData, onPutCompleteHandler); + dataSourceUpdates.init(initData, onPutCompleteHandler, initMetadataFromHeaders(initHeaders)); }, }); diff --git a/packages/shared/sdk-server/src/hooks/HookRunner.ts b/packages/shared/sdk-server/src/hooks/HookRunner.ts index c7c1e61f4f..315970f1b4 100644 --- a/packages/shared/sdk-server/src/hooks/HookRunner.ts +++ b/packages/shared/sdk-server/src/hooks/HookRunner.ts @@ -22,6 +22,7 @@ export default class HookRunner { defaultValue: unknown, methodName: string, method: () => Promise, + environmentId?: string, ): Promise { // This early return is here to avoid the extra async/await associated with // using withHooksDataWithDetail. @@ -38,6 +39,7 @@ export default class HookRunner { const detail = await method(); return { detail }; }, + environmentId, ).then(({ detail }) => detail); } @@ -51,12 +53,13 @@ export default class HookRunner { defaultValue: unknown, methodName: string, method: () => Promise<{ detail: LDEvaluationDetail; [index: string]: any }>, + environmentId?: string, ): Promise<{ detail: LDEvaluationDetail; [index: string]: any }> { if (this._hooks.length === 0) { return method(); } const { hooks, hookContext }: { hooks: Hook[]; hookContext: EvaluationSeriesContext } = - this._prepareHooks(key, context, defaultValue, methodName); + this._prepareHooks(key, context, defaultValue, methodName, environmentId); const hookData = this._executeBeforeEvaluation(hooks, hookContext); const result = await method(); this._executeAfterEvaluation(hooks, hookContext, hookData, result.detail); @@ -124,6 +127,7 @@ export default class HookRunner { context: LDContext, defaultValue: unknown, methodName: string, + environmentId?: string, ): { hooks: Hook[]; hookContext: EvaluationSeriesContext; @@ -137,6 +141,7 @@ export default class HookRunner { context, defaultValue, method: methodName, + environmentId, }; return { hooks, hookContext }; } diff --git a/packages/shared/sdk-server/src/store/AsyncStoreFacade.ts b/packages/shared/sdk-server/src/store/AsyncStoreFacade.ts index 5d24d81997..d49eddb81c 100644 --- a/packages/shared/sdk-server/src/store/AsyncStoreFacade.ts +++ b/packages/shared/sdk-server/src/store/AsyncStoreFacade.ts @@ -1,3 +1,5 @@ +import { internal } from '@launchdarkly/js-sdk-common'; + import { DataKind } from '../api/interfaces'; import { LDFeatureStore, @@ -8,6 +10,8 @@ import { } from '../api/subsystems'; import promisify from '../async/promisify'; +type InitMetadata = internal.InitMetadata; + /** * Provides an async interface to a feature store. * @@ -33,9 +37,9 @@ export default class AsyncStoreFacade { }); } - async init(allData: LDFeatureStoreDataStorage): Promise { + async init(allData: LDFeatureStoreDataStorage, initMetadata?: InitMetadata): Promise { return promisify((cb) => { - this._store.init(allData, cb); + this._store.init(allData, cb, initMetadata); }); } @@ -60,4 +64,8 @@ export default class AsyncStoreFacade { close(): void { this._store.close(); } + + getInitMetadata?(): InitMetadata | undefined { + return this._store.getInitMetaData?.(); + } } diff --git a/packages/shared/sdk-server/src/store/InMemoryFeatureStore.ts b/packages/shared/sdk-server/src/store/InMemoryFeatureStore.ts index 61814f2aac..30d9d6db7c 100644 --- a/packages/shared/sdk-server/src/store/InMemoryFeatureStore.ts +++ b/packages/shared/sdk-server/src/store/InMemoryFeatureStore.ts @@ -1,3 +1,5 @@ +import { internal } from '@launchdarkly/js-sdk-common'; + import { DataKind } from '../api/interfaces'; import { LDFeatureStore, @@ -7,11 +9,15 @@ import { LDKeyedFeatureStoreItem, } from '../api/subsystems'; +type InitMetadata = internal.InitMetadata; + export default class InMemoryFeatureStore implements LDFeatureStore { private _allData: LDFeatureStoreDataStorage = {}; private _initCalled = false; + private _initMetadata?: InitMetadata; + private _addItem(kind: DataKind, key: string, item: LDFeatureStoreItem) { let items = this._allData[kind.namespace]; if (!items) { @@ -52,9 +58,14 @@ export default class InMemoryFeatureStore implements LDFeatureStore { callback?.(result); } - init(allData: LDFeatureStoreDataStorage, callback: () => void): void { + init( + allData: LDFeatureStoreDataStorage, + callback: () => void, + initMetadata?: InitMetadata, + ): void { this._initCalled = true; this._allData = allData as LDFeatureStoreDataStorage; + this._initMetadata = initMetadata; callback?.(); } @@ -81,4 +92,8 @@ export default class InMemoryFeatureStore implements LDFeatureStore { getDescription(): string { return 'memory'; } + + getInitMetaData(): InitMetadata | undefined { + return this._initMetadata; + } } diff --git a/packages/store/node-server-sdk-dynamodb/CHANGELOG.md b/packages/store/node-server-sdk-dynamodb/CHANGELOG.md index e691201bd4..aec808276c 100644 --- a/packages/store/node-server-sdk-dynamodb/CHANGELOG.md +++ b/packages/store/node-server-sdk-dynamodb/CHANGELOG.md @@ -90,6 +90,17 @@ * devDependencies * @launchdarkly/node-server-sdk bumped from 9.2.1 to 9.2.2 +## [6.2.9](https://github.com/launchdarkly/js-core/compare/node-server-sdk-dynamodb-v6.2.8...node-server-sdk-dynamodb-v6.2.9) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.8.0 to 9.9.0 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.9.0 + ## [6.2.8](https://github.com/launchdarkly/js-core/compare/node-server-sdk-dynamodb-v6.2.7...node-server-sdk-dynamodb-v6.2.8) (2025-04-08) diff --git a/packages/store/node-server-sdk-dynamodb/package.json b/packages/store/node-server-sdk-dynamodb/package.json index 4afaf6e02b..7bcb339980 100644 --- a/packages/store/node-server-sdk-dynamodb/package.json +++ b/packages/store/node-server-sdk-dynamodb/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/node-server-sdk-dynamodb", - "version": "6.2.8", + "version": "6.2.9", "description": "DynamoDB-backed feature store for the LaunchDarkly Server-Side SDK for Node.js", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/store/node-server-sdk-dynamodb", "repository": { @@ -35,7 +35,7 @@ }, "devDependencies": { "@aws-sdk/client-dynamodb": "3.348.0", - "@launchdarkly/node-server-sdk": "9.8.0", + "@launchdarkly/node-server-sdk": "9.9.0", "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/jest": "^29.4.0", "@typescript-eslint/eslint-plugin": "^6.20.0", diff --git a/packages/store/node-server-sdk-redis/CHANGELOG.md b/packages/store/node-server-sdk-redis/CHANGELOG.md index bcf2d876cd..5a6e6a58ab 100644 --- a/packages/store/node-server-sdk-redis/CHANGELOG.md +++ b/packages/store/node-server-sdk-redis/CHANGELOG.md @@ -90,6 +90,17 @@ * devDependencies * @launchdarkly/node-server-sdk bumped from 9.2.1 to 9.2.2 +## [4.2.9](https://github.com/launchdarkly/js-core/compare/node-server-sdk-redis-v4.2.8...node-server-sdk-redis-v4.2.9) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.8.0 to 9.9.0 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.9.0 + ## [4.2.8](https://github.com/launchdarkly/js-core/compare/node-server-sdk-redis-v4.2.7...node-server-sdk-redis-v4.2.8) (2025-04-08) diff --git a/packages/store/node-server-sdk-redis/package.json b/packages/store/node-server-sdk-redis/package.json index 05c0feed56..51ac0147d8 100644 --- a/packages/store/node-server-sdk-redis/package.json +++ b/packages/store/node-server-sdk-redis/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/node-server-sdk-redis", - "version": "4.2.8", + "version": "4.2.9", "description": "Redis-backed feature store for the LaunchDarkly Server-Side SDK for Node.js", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/store/node-server-sdk-redis", "repository": { @@ -33,7 +33,7 @@ "@launchdarkly/node-server-sdk": ">=9.4.3" }, "devDependencies": { - "@launchdarkly/node-server-sdk": "9.8.0", + "@launchdarkly/node-server-sdk": "9.9.0", "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/jest": "^29.4.0", "@typescript-eslint/eslint-plugin": "^6.20.0", diff --git a/packages/telemetry/browser-telemetry/CHANGELOG.md b/packages/telemetry/browser-telemetry/CHANGELOG.md index 1c2f04429f..6ff46da04e 100644 --- a/packages/telemetry/browser-telemetry/CHANGELOG.md +++ b/packages/telemetry/browser-telemetry/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [1.0.6](https://github.com/launchdarkly/js-core/compare/browser-telemetry-v1.0.5...browser-telemetry-v1.0.6) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/js-client-sdk bumped from 0.5.2 to 0.5.3 + +## [1.0.5](https://github.com/launchdarkly/js-core/compare/browser-telemetry-v1.0.4...browser-telemetry-v1.0.5) (2025-04-15) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/js-client-sdk bumped from 0.5.1 to 0.5.2 + ## [1.0.4](https://github.com/launchdarkly/js-core/compare/browser-telemetry-v1.0.3...browser-telemetry-v1.0.4) (2025-04-08) diff --git a/packages/telemetry/browser-telemetry/package.json b/packages/telemetry/browser-telemetry/package.json index ae97f4557a..b6a49eb791 100644 --- a/packages/telemetry/browser-telemetry/package.json +++ b/packages/telemetry/browser-telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/browser-telemetry", - "version": "1.0.4", + "version": "1.0.6", "packageManager": "yarn@3.4.1", "type": "module", "main": "./dist/index.cjs", @@ -45,7 +45,7 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", - "@launchdarkly/js-client-sdk": "0.5.1", + "@launchdarkly/js-client-sdk": "0.5.3", "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/css-font-loading-module": "^0.0.13", "@types/jest": "^29.5.11", diff --git a/packages/telemetry/node-server-sdk-otel/CHANGELOG.md b/packages/telemetry/node-server-sdk-otel/CHANGELOG.md index 39f6c2b6d2..f840c05d64 100644 --- a/packages/telemetry/node-server-sdk-otel/CHANGELOG.md +++ b/packages/telemetry/node-server-sdk-otel/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [1.2.0](https://github.com/launchdarkly/js-core/compare/node-server-sdk-otel-v1.1.8...node-server-sdk-otel-v1.2.0) (2025-04-16) + + +### Features + +* Environment ID support for hooks ([#823](https://github.com/launchdarkly/js-core/issues/823)) ([63dc9f9](https://github.com/launchdarkly/js-core/commit/63dc9f9f1300c598e79be27909f8195ac66d54ef)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.8.0 to 9.9.0 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.9.0 + ## [1.1.8](https://github.com/launchdarkly/js-core/compare/node-server-sdk-otel-v1.1.7...node-server-sdk-otel-v1.1.8) (2025-04-08) diff --git a/packages/telemetry/node-server-sdk-otel/__tests__/TracingHook.test.ts b/packages/telemetry/node-server-sdk-otel/__tests__/TracingHook.test.ts index edc110b1ec..de1cd1325c 100644 --- a/packages/telemetry/node-server-sdk-otel/__tests__/TracingHook.test.ts +++ b/packages/telemetry/node-server-sdk-otel/__tests__/TracingHook.test.ts @@ -26,15 +26,20 @@ it('validates configuration', async () => { messages.push(text); }, }), + // @ts-ignore + environmentId: 12345, }); - expect(messages.length).toEqual(2); + expect(messages.length).toEqual(3); expect(messages[0]).toEqual( 'error: [LaunchDarkly] Config option "includeVariant" should be of type boolean, got string, using default value', ); expect(messages[1]).toEqual( 'error: [LaunchDarkly] Config option "spans" should be of type boolean, got string, using default value', ); + expect(messages[2]).toEqual( + 'error: [LaunchDarkly] Config option "environmentId" should be of type string, got number, using default value', + ); }); it('instance can be created with default config', () => { @@ -72,6 +77,7 @@ describe('with a testing otel span collector', () => { expect(spanEvent.attributes!['feature_flag.provider_name']).toEqual('LaunchDarkly'); expect(spanEvent.attributes!['feature_flag.context.key']).toEqual('user-key'); expect(spanEvent.attributes!['feature_flag.variant']).toBeUndefined(); + expect(spanEvent.attributes!['feature_flag.set.id']).toBeUndefined(); }); it('can include variant in span events', async () => { @@ -135,4 +141,81 @@ describe('with a testing otel span collector', () => { const spanEvent = spans[0]!.events[0]!; expect(spanEvent.attributes!['feature_flag.context.key']).toEqual('org:org-key:user:bob'); }); + + it('can include environmentId from options', async () => { + const td = new integrations.TestData(); + const client = init('bad-key', { + sendEvents: false, + updateProcessor: td.getFactory(), + hooks: [new TracingHook({ environmentId: 'id-from-options' })], + }); + + const tracer = trace.getTracer('trace-hook-test-tracer'); + await tracer.startActiveSpan('test-span', { root: true }, async (span) => { + await client.boolVariation('test-bool', { kind: 'user', key: 'user-key' }, false); + span.end(); + }); + + const spans = spanExporter.getFinishedSpans(); + const spanEvent = spans[0]!.events[0]!; + expect(spanEvent.attributes!['feature_flag.set.id']).toEqual('id-from-options'); + }); + + it('can include environmentId from hook context', async () => { + const hook = new TracingHook(); + const td = new integrations.TestData(); + const client = init('bad-key', { + sendEvents: false, + updateProcessor: td.getFactory(), + hooks: [hook], + }); + + jest.spyOn(hook, 'afterEvaluation').mockImplementationOnce((hookContext, data, detail) => + // @ts-ignore + hook.afterEvaluation?.( + { ...hookContext, environmentId: 'id-from-hook-context' }, + data, + detail, + ), + ); + + const tracer = trace.getTracer('trace-hook-test-tracer'); + await tracer.startActiveSpan('test-span', { root: true }, async (span) => { + await client.boolVariation('test-bool', { kind: 'user', key: 'user-key' }, false); + span.end(); + }); + + const spans = spanExporter.getFinishedSpans(); + const spanEvent = spans[0]!.events[0]!; + expect(spanEvent.attributes!['feature_flag.set.id']).toEqual('id-from-hook-context'); + }); + + it('can override hook context environmentId with options', async () => { + const hook = new TracingHook({ environmentId: 'id-from-options' }); + const td = new integrations.TestData(); + const client = init('bad-key', { + sendEvents: false, + updateProcessor: td.getFactory(), + hooks: [hook], + }); + + jest.spyOn(hook, 'afterEvaluation').mockImplementationOnce((hookContext, data, detail) => + // @ts-ignore + hook.afterEvaluation?.( + { ...hookContext, environmentId: 'id-from-hook-context' }, + data, + detail, + ), + ); + + const tracer = trace.getTracer('trace-hook-test-tracer'); + await tracer.startActiveSpan('test-span', { root: true }, async (span) => { + await client.boolVariation('test-bool', { kind: 'user', key: 'user-key' }, false); + span.end(); + }); + + const spans = spanExporter.getFinishedSpans(); + const spanEvent = spans[0]!.events[0]!; + expect(spanEvent.attributes!['feature_flag.set.id']).toEqual('id-from-options'); + }); }); diff --git a/packages/telemetry/node-server-sdk-otel/package.json b/packages/telemetry/node-server-sdk-otel/package.json index 3433b4b13c..4f8432197f 100644 --- a/packages/telemetry/node-server-sdk-otel/package.json +++ b/packages/telemetry/node-server-sdk-otel/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/node-server-sdk-otel", - "version": "1.1.8", + "version": "1.2.0", "type": "commonjs", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -33,7 +33,7 @@ "@opentelemetry/api": ">=1.3.0" }, "devDependencies": { - "@launchdarkly/node-server-sdk": "9.8.0", + "@launchdarkly/node-server-sdk": "9.9.0", "@opentelemetry/api": ">=1.3.0", "@opentelemetry/sdk-node": "0.49.1", "@opentelemetry/sdk-trace-node": "1.22.0", diff --git a/packages/telemetry/node-server-sdk-otel/src/TracingHook.ts b/packages/telemetry/node-server-sdk-otel/src/TracingHook.ts index 087464c5cb..35d5911a3f 100644 --- a/packages/telemetry/node-server-sdk-otel/src/TracingHook.ts +++ b/packages/telemetry/node-server-sdk-otel/src/TracingHook.ts @@ -17,6 +17,7 @@ const FEATURE_FLAG_KEY_ATTR = `${FEATURE_FLAG_SCOPE}.key`; const FEATURE_FLAG_PROVIDER_ATTR = `${FEATURE_FLAG_SCOPE}.provider_name`; const FEATURE_FLAG_CONTEXT_KEY_ATTR = `${FEATURE_FLAG_SCOPE}.context.key`; const FEATURE_FLAG_VARIANT_ATTR = `${FEATURE_FLAG_SCOPE}.variant`; +const FEATURE_FLAG_SET_ID = `${FEATURE_FLAG_SCOPE}.set.id`; const TRACING_HOOK_NAME = 'LaunchDarkly Tracing Hook'; @@ -49,12 +50,15 @@ export interface TracingHookOptions { * using `console`. */ logger?: LDLogger; + + environmentId?: string; } interface ValidatedHookOptions { spans: boolean; includeVariant: boolean; logger: LDLogger; + environmentId?: string; } type SpanTraceData = { @@ -65,6 +69,7 @@ const defaultOptions: ValidatedHookOptions = { spans: false, includeVariant: false, logger: basicLogger({ name: TRACING_HOOK_NAME }), + environmentId: undefined, }; function validateOptions(options?: TracingHookOptions): ValidatedHookOptions { @@ -94,6 +99,16 @@ function validateOptions(options?: TracingHookOptions): ValidatedHookOptions { } } + if (options?.environmentId !== undefined) { + if (TypeValidators.String.is(options.environmentId)) { + validatedOptions.environmentId = options.environmentId; + } else { + validatedOptions.logger.error( + OptionMessages.wrongOptionType('environmentId', 'string', typeof options?.environmentId), + ); + } + } + return validatedOptions; } @@ -163,6 +178,11 @@ export default class TracingHook implements integrations.Hook { [FEATURE_FLAG_PROVIDER_ATTR]: 'LaunchDarkly', [FEATURE_FLAG_CONTEXT_KEY_ATTR]: Context.fromLDContext(hookContext.context).canonicalKey, }; + if (this._options.environmentId) { + eventAttributes[FEATURE_FLAG_SET_ID] = this._options.environmentId; + } else if (hookContext.environmentId) { + eventAttributes[FEATURE_FLAG_SET_ID] = hookContext.environmentId; + } if (this._options.includeVariant) { eventAttributes[FEATURE_FLAG_VARIANT_ATTR] = JSON.stringify(detail.value); } diff --git a/packages/tooling/jest/CHANGELOG.md b/packages/tooling/jest/CHANGELOG.md index 8fc5ca2901..eef8c71c61 100644 --- a/packages/tooling/jest/CHANGELOG.md +++ b/packages/tooling/jest/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [0.1.4](https://github.com/launchdarkly/js-core/compare/jest-v0.1.3...jest-v0.1.4) (2025-04-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/react-native-client-sdk bumped from ~10.9.8 to ~10.9.9 + ## [0.1.3](https://github.com/launchdarkly/js-core/compare/jest-v0.1.2...jest-v0.1.3) (2025-04-08) diff --git a/packages/tooling/jest/package.json b/packages/tooling/jest/package.json index 4d704cc9d6..14aa86d5e1 100644 --- a/packages/tooling/jest/package.json +++ b/packages/tooling/jest/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/jest", - "version": "0.1.3", + "version": "0.1.4", "description": "Easily unit test LaunchDarkly feature flagged components with jest", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/tooling/jest", "repository": { @@ -62,7 +62,7 @@ "typescript": "5.1.6" }, "dependencies": { - "@launchdarkly/react-native-client-sdk": "~10.9.8", + "@launchdarkly/react-native-client-sdk": "~10.9.9", "@testing-library/react-hooks": "^8.0.1", "@testing-library/react-native": "^12.7.2", "@types/lodash": "^4.17.7", diff --git a/tsconfig.json b/tsconfig.json index f6ff5c4506..f1fcd6a232 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -72,6 +72,9 @@ }, { "path": "./packages/sdk/fastly/tsconfig.ref.json" + }, + { + "path": "./contract-tests/tsconfig.ref.json" } ] }