From df7b6f0f066a3f39311a766899cc5487c7a13607 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 15 Jan 2025 13:27:26 +0100 Subject: [PATCH 01/11] meta: pin workflow version again (#236) This commit re-pins the workflow version for the release template reference. It uses an assumed 2.0 which is what we'd need to bump it to for changing the parameters. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9e3f25..16adfd1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ on: jobs: from-template: - uses: Unleash/.github/.github/workflows/npm-release.yml@meta/update-npm-release-to-use-github-bot + uses: Unleash/.github/.github/workflows/npm-release.yml@v2.0.0 with: version: ${{ github.event.inputs.version }} tag: ${{ github.event.inputs.tag }} From b2e385dddb09726fb8d62624ffaabeb9ff6c1ea2 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 16 Jan 2025 08:14:44 +0100 Subject: [PATCH 02/11] fix: embed version at compile time (#237) Removes requiring version at runtime with require: 3615f15#diff-258035e5968f6bf645400d417f310218d7d9a9a10606a3c34e7f55db193f58f3R3 Instead we're using the rollup replace plugin to read the version at build time and replace __VERSION__ placeholder with the actual version number. As a result other packages depending on this one don't need to understand require calls and also we're not exposing the whole package.json file at runtime. To verify my claim above you can build the package and check the build directory after this change vs before this change. --- .gitignore | 1 + package.json | 1 + rollup.config.mjs | 8 ++++++++ src/index.test.ts | 5 ++--- src/index.ts | 6 ++---- src/metrics.ts | 5 ++--- src/version.ts | 1 + yarn.lock | 14 +++++++++++--- 8 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 src/version.ts diff --git a/.gitignore b/.gitignore index c274848..fba9326 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ .vscode/ coverage/ .idea +.DS_Store diff --git a/package.json b/package.json index 2202bd3..3f37f1d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@babel/runtime": "^7.23.1", "@rollup/plugin-commonjs": "^25.0.5", "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.5", "@types/jest": "^29.5.5", diff --git a/rollup.config.mjs b/rollup.config.mjs index 6b176b4..9fb2001 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -3,6 +3,10 @@ import nodeResolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import terser from '@rollup/plugin-terser'; import nodePolyfills from 'rollup-plugin-node-polyfills'; +import replace from '@rollup/plugin-replace'; +import fs from 'fs'; + +const version = JSON.parse(fs.readFileSync('./package.json', 'UTF-8')).version; export default { input: './src/index.ts', @@ -25,6 +29,10 @@ export default { } ], plugins: [ + replace({ + '__VERSION__': version, + preventAssignment: true + }), typescript({ compilerOptions: { lib: ['es5', 'es6', 'dom'], diff --git a/src/index.test.ts b/src/index.test.ts index 8c4ef3c..fb3677d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,6 +1,4 @@ import { FetchMock } from 'jest-fetch-mock'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const packageJSON = require('../package.json'); import 'jest-localstorage-mock'; import * as data from './test/testdata.json'; import IStorageProvider from './storage-provider'; @@ -1383,7 +1381,8 @@ test('Should add `x-unleash` headers', async () => { /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const expectedHeaders = { - 'x-unleash-sdk': `unleash-js@${packageJSON.version}`, + // will be replaced at build time with the actual version + 'x-unleash-sdk': 'unleash-js@__VERSION__', 'x-unleash-connection-id': expect.stringMatching(uuidFormat), 'x-unleash-appname': appName, }; diff --git a/src/index.ts b/src/index.ts index 7e82219..748f00e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,9 +10,7 @@ import { notNullOrUndefined, urlWithContextAsQuery, } from './util'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const packageJSON = require('../package.json'); +import { sdkVersion } from './version'; const DEFINED_FIELDS = [ 'userId', @@ -476,7 +474,7 @@ export class UnleashClient extends TinyEmitter { const headers = { [this.headerName]: this.clientKey, Accept: 'application/json', - 'x-unleash-sdk': `unleash-js@${packageJSON.version}`, + 'x-unleash-sdk': sdkVersion, 'x-unleash-connection-id': this.connectionId, 'x-unleash-appname': this.context.appName, }; diff --git a/src/metrics.ts b/src/metrics.ts index 29e2c9f..c769163 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,8 +1,7 @@ // Simplified version of: https://github.com/Unleash/unleash-client-node/blob/main/src/metrics.ts import { notNullOrUndefined } from './util'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const packageJSON = require('../package.json'); +import { sdkVersion } from './version'; export interface MetricsOptions { onError: OnError; @@ -127,7 +126,7 @@ export default class Metrics { [this.headerName]: this.clientKey, Accept: 'application/json', 'Content-Type': 'application/json', - 'x-unleash-sdk': `unleash-js@${packageJSON.version}`, + 'x-unleash-sdk': sdkVersion, 'x-unleash-connection-id': this.connectionId, 'x-unleash-appname': this.appName, }; diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..896c627 --- /dev/null +++ b/src/version.ts @@ -0,0 +1 @@ +export const sdkVersion = `unleash-js@__VERSION__`; diff --git a/yarn.lock b/yarn.lock index cf404e4..7dce0f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -642,6 +642,14 @@ is-module "^1.0.0" resolve "^1.22.1" +"@rollup/plugin-replace@^6.0.2": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-6.0.2.tgz#2f565d312d681e4570ff376c55c5c08eb6f1908d" + integrity sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ== + dependencies: + "@rollup/pluginutils" "^5.0.1" + magic-string "^0.30.3" + "@rollup/plugin-terser@^0.4.4": version "0.4.4" resolved "https://registry.yarnpkg.com/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz#15dffdb3f73f121aa4fbb37e7ca6be9aeea91962" @@ -2977,9 +2985,9 @@ rollup-pluginutils@^2.8.1: estree-walker "^0.6.1" rollup@^3.29.4: - version "3.29.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" - integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + version "3.29.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54" + integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w== optionalDependencies: fsevents "~2.3.2" From e204714eb4058ccb983d6b283c3e9b9edc10ebf9 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Thu, 16 Jan 2025 07:17:56 +0000 Subject: [PATCH 03/11] v3.7.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3f37f1d..eb5828a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unleash-proxy-client", - "version": "3.7.0", + "version": "3.7.1", "description": "A browser client that can be used together with Unleash Edge or the Unleash Frontend API.", "type": "module", "main": "./build/index.cjs", From 40daee5a19b9f99dccd2b382a80502a54cb465b9 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 16 Jan 2025 13:25:23 +0100 Subject: [PATCH 04/11] chore(1-3230): use homebrew version of uuid generation (#240) The one from the uuid library relies on an underlying crypto library which doesn't exist at least in certain GitHub runners and may also cause issues for react native applications. This impl sidesteps that issue. The same uuid generation method that we're cutting out here is also being used in src/events-handler.ts. However, because we haven't received any complaints about that, I'll leave it in. --- src/index.ts | 2 +- src/uuidv4.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/uuidv4.ts diff --git a/src/index.ts b/src/index.ts index 748f00e..eff6f41 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import { TinyEmitter } from 'tiny-emitter'; -import { v4 as uuidv4 } from 'uuid'; import Metrics from './metrics'; import type IStorageProvider from './storage-provider'; import InMemoryStorageProvider from './storage-provider-inmemory'; @@ -11,6 +10,7 @@ import { urlWithContextAsQuery, } from './util'; import { sdkVersion } from './version'; +import { uuidv4 } from './uuidv4'; const DEFINED_FIELDS = [ 'userId', diff --git a/src/uuidv4.ts b/src/uuidv4.ts new file mode 100644 index 0000000..7929359 --- /dev/null +++ b/src/uuidv4.ts @@ -0,0 +1,14 @@ +/** + * This function generates a UUID using Math.random(). + * The distribution of unique values is not guaranteed to be as robust + * as with a crypto module but works across all platforms (Node, React Native, browser JS). + * + * We use it for connection id generation which is not critical for security. + */ +export const uuidv4 = (): string => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}; From bf26e94fb18b1b5d5b5cae4753bbee1f75ba9535 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Thu, 16 Jan 2025 12:27:08 +0000 Subject: [PATCH 05/11] v3.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb5828a..40ea932 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unleash-proxy-client", - "version": "3.7.1", + "version": "3.7.2", "description": "A browser client that can be used together with Unleash Edge or the Unleash Frontend API.", "type": "module", "main": "./build/index.cjs", From 80af67fdcace204ee4b50dc1c565df46ebfa613a Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 31 Jan 2025 09:18:51 +0100 Subject: [PATCH 06/11] fix: improve headers parsing (#243) --- src/index.test.ts | 24 ++++----- src/index.ts | 30 ++++-------- src/metrics.test.ts | 35 ++++++++++++- src/metrics.ts | 25 ++++------ src/util.test.ts | 117 ++++++++++++++++++++++++++++++++++++++++++++ src/util.ts | 44 +++++++++++++++++ src/version.ts | 2 +- 7 files changed, 226 insertions(+), 51 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index fb3677d..e28529b 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -64,7 +64,7 @@ test('Should perform an initial fetch as POST', async () => { expect(request.method).toBe('POST'); expect(body.context.appName).toBe('webAsPOST'); expect(request.headers).toMatchObject({ - 'Content-Type': 'application/json', + 'content-type': 'application/json', }); }); @@ -650,7 +650,7 @@ test('Should abort previous request', async () => { client.updateContext({ userId: '456' }); // abort 2 await client.updateContext({ userId: '789' }); - expect(abortSpy).toBeCalledTimes(2); + expect(abortSpy).toHaveBeenCalledTimes(2); abortSpy.mockRestore(); }); @@ -690,7 +690,7 @@ test('Should run without abort controller', async () => { client.updateContext({ userId: '456' }); await client.updateContext({ userId: '789' }); - expect(abortSpy).toBeCalledTimes(0); + expect(abortSpy).toHaveBeenCalledTimes(0); abortSpy.mockRestore(); }); @@ -783,7 +783,7 @@ test('Should include etag in second request', async () => { expect(firstRequest.headers).toMatchObject({}); expect(secondRequest.headers).toMatchObject({ - 'If-None-Match': etag, + 'if-none-match': etag, }); }); @@ -805,7 +805,7 @@ test('Should add clientKey as Authorization header', async () => { const request = getTypeSafeRequest(fetchMock); expect(request.headers).toMatchObject({ - Authorization: 'some123key', + authorization: 'some123key', }); }); @@ -1037,7 +1037,7 @@ test('Updating context should wait on asynchronous start', async () => { userId: '123', }); - expect(fetchMock).toBeCalledTimes(2); + expect(fetchMock).toHaveBeenCalledTimes(2); }); test('Should not replace sessionId when updating context', async () => { @@ -1254,7 +1254,7 @@ test('Initializing client twice should show a console warning', async () => { await client.start(); await client.start(); // Expect console.error to be called once before start runs. - expect(console.error).toBeCalledTimes(2); + expect(console.error).toHaveBeenCalledTimes(2); }); test('Should pass under custom header clientKey', async () => { @@ -1356,7 +1356,7 @@ test('Should pass custom headers', async () => { }); }); -test('Should add `x-unleash` headers', async () => { +test('Should add unleash identification headers', async () => { fetchMock.mockResponses( [JSON.stringify(data), { status: 200 }], [JSON.stringify(data), { status: 200 }] @@ -1382,13 +1382,13 @@ test('Should add `x-unleash` headers', async () => { const expectedHeaders = { // will be replaced at build time with the actual version - 'x-unleash-sdk': 'unleash-js@__VERSION__', - 'x-unleash-connection-id': expect.stringMatching(uuidFormat), - 'x-unleash-appname': appName, + 'unleash-sdk': 'unleash-client-js:__VERSION__', + 'unleash-connection-id': expect.stringMatching(uuidFormat), + 'unleash-appname': appName, }; const getConnectionId = (request: any) => - request.headers['x-unleash-connection-id']; + request.headers['unleash-connection-id']; expect(featureRequest.headers).toMatchObject(expectedHeaders); expect(metricsRequest.headers).toMatchObject(expectedHeaders); diff --git a/src/index.ts b/src/index.ts index eff6f41..9f29185 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,10 +6,9 @@ import LocalStorageProvider from './storage-provider-local'; import EventsHandler from './events-handler'; import { computeContextHashValue, - notNullOrUndefined, + parseHeaders, urlWithContextAsQuery, } from './util'; -import { sdkVersion } from './version'; import { uuidv4 } from './uuidv4'; const DEFINED_FIELDS = [ @@ -470,24 +469,15 @@ export class UnleashClient extends TinyEmitter { } private getHeaders() { - const isPOST = this.usePOSTrequests; - const headers = { - [this.headerName]: this.clientKey, - Accept: 'application/json', - 'x-unleash-sdk': sdkVersion, - 'x-unleash-connection-id': this.connectionId, - 'x-unleash-appname': this.context.appName, - }; - if (isPOST) { - headers['Content-Type'] = 'application/json'; - } - if (this.etag) { - headers['If-None-Match'] = this.etag; - } - Object.entries(this.customHeaders) - .filter(notNullOrUndefined) - .forEach(([name, value]) => (headers[name] = value)); - return headers; + return parseHeaders({ + clientKey: this.clientKey, + connectionId: this.connectionId, + appName: this.context.appName, + customHeaders: this.customHeaders, + headerName: this.headerName, + etag: this.etag, + isPost: this.usePOSTrequests, + }); } private async storeToggles(toggles: IToggle[]): Promise { diff --git a/src/metrics.test.ts b/src/metrics.test.ts index a2f9dc6..3702e66 100644 --- a/src/metrics.test.ts +++ b/src/metrics.test.ts @@ -91,7 +91,7 @@ test('should send metrics with custom auth header', async () => { expect(fetchMock.mock.calls.length).toEqual(1); expect(requestBody.headers).toMatchObject({ - NotAuthorization: '123', + notauthorization: '123', }); }); @@ -271,7 +271,7 @@ describe('Custom headers for metrics', () => { test('Custom headers should override preset headers', async () => { const customHeaders = { - Authorization: 'definitely-not-the-client-key', + authorization: 'definitely-not-the-client-key', }; const requestBody = await runMetrics(customHeaders); @@ -303,4 +303,35 @@ describe('Custom headers for metrics', () => { expect(requestBody.headers).not.toMatchObject(customHeaders); } ); + + test('Should use case-insensitive headers', () => { + const metrics = new Metrics({ + onError: console.error, + appName: 'test', + metricsInterval: 5, + disableMetrics: false, + url: 'http://localhost:3000', + clientKey: '123', + fetch: fetchMock, + headerName: 'Authorization', + customHeaders: { + 'Custom-Header': '123', + 'custom-header': '456', + 'unleash-APPname': 'override', + 'unleash-connection-id': 'override', + }, + connectionId: '123', + metricsIntervalInitial: 2, + }); + + metrics.count('foo', true); + metrics.sendMetrics(); + + const requestBody = getTypeSafeRequest(fetchMock); + expect(requestBody.headers).toMatchObject({ + 'custom-header': '456', + 'unleash-appname': 'override', + 'unleash-connection-id': '123', + }); + }); }); diff --git a/src/metrics.ts b/src/metrics.ts index c769163..0ef8c55 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,7 +1,6 @@ // Simplified version of: https://github.com/Unleash/unleash-client-node/blob/main/src/metrics.ts -import { notNullOrUndefined } from './util'; -import { sdkVersion } from './version'; +import { parseHeaders } from './util'; export interface MetricsOptions { onError: OnError; @@ -122,20 +121,14 @@ export default class Metrics { } private getHeaders() { - const headers = { - [this.headerName]: this.clientKey, - Accept: 'application/json', - 'Content-Type': 'application/json', - 'x-unleash-sdk': sdkVersion, - 'x-unleash-connection-id': this.connectionId, - 'x-unleash-appname': this.appName, - }; - - Object.entries(this.customHeaders) - .filter(notNullOrUndefined) - .forEach(([name, value]) => (headers[name] = value)); - - return headers; + return parseHeaders({ + clientKey: this.clientKey, + appName: this.appName, + connectionId: this.connectionId, + customHeaders: this.customHeaders, + headerName: this.headerName, + isPost: true, + }); } public async sendMetrics(): Promise { diff --git a/src/util.test.ts b/src/util.test.ts index a05fe4f..f846dd3 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -3,6 +3,7 @@ import { computeContextHashValue, contextString, urlWithContextAsQuery, + parseHeaders, } from './util'; test('should not add paramters to URL', async () => { @@ -119,3 +120,119 @@ describe('computeContextHashValue', () => { ); }); }); + +describe('parseHeaders', () => { + const clientKey = 'test-client-key'; + const appName = 'appName'; + const connectionId = '1234-5678'; + + test('should set basic headers correctly', () => { + const result = parseHeaders({ + clientKey, + appName, + connectionId, + }); + + expect(result).toEqual({ + authorization: clientKey, + 'unleash-sdk': 'unleash-client-js:__VERSION__', + 'unleash-appname': appName, + accept: 'application/json', + 'unleash-connection-id': connectionId, + }); + }); + + test('should set custom client key header name', () => { + const result = parseHeaders({ + clientKey, + appName, + connectionId, + headerName: 'auth', + }); + + expect(result).toMatchObject({ auth: clientKey }); + expect(Object.keys(result)).not.toContain('authorization'); + }); + + test('should add custom headers', () => { + const customHeaders = { + 'custom-header': 'custom-value', + 'unleash-connection-id': 'should-not-be-overwritten', + 'unleash-appname': 'new-app-name', + }; + const result = parseHeaders({ + clientKey, + appName, + connectionId, + customHeaders, + }); + + expect(Object.keys(result)).toHaveLength(6); + expect(result).toMatchObject({ + 'custom-header': 'custom-value', + 'unleash-connection-id': connectionId, + 'unleash-appname': 'new-app-name', + }); + }); + + test('should include etag if provided', () => { + const etag = '12345'; + const result = parseHeaders({ + clientKey, + appName, + connectionId, + etag, + }); + + expect(result['if-none-match']).toBe(etag); + }); + + test('should handle custom headers in a case-insensitive way and normalize them', () => { + const customHeaders = { + 'custom-HEADER': 'custom-value-1', + 'Custom-Header': 'custom-value-2', + }; + const result = parseHeaders({ + clientKey, + appName, + connectionId, + customHeaders, + }); + + expect(result).toMatchObject({ + 'custom-header': 'custom-value-2', + }); + }); + + test('should ignore null or undefined custom headers', () => { + const customHeaders = { + header1: 'value1', + header2: null as any, + header3: undefined as any, + }; + const result = parseHeaders({ + clientKey, + appName, + connectionId, + customHeaders, + }); + + expect(result).toEqual( + expect.not.objectContaining({ + header2: expect.anything(), + header3: expect.anything(), + }) + ); + }); + + test('should include content-type for POST requests', () => { + const result = parseHeaders({ + clientKey, + appName, + connectionId, + isPost: true, + }); + + expect(result['content-type']).toBe('application/json'); + }); +}); diff --git a/src/util.ts b/src/util.ts index 7a6c35b..3839d66 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,5 @@ import { IContext } from '.'; +import { sdkVersion } from './version'; export const notNullOrUndefined = ([, value]: [string, string]) => value !== undefined && value !== null; @@ -70,3 +71,46 @@ export const computeContextHashValue = async (obj: IContext) => { return value; } }; + +export const parseHeaders = ({ + clientKey, + appName, + connectionId, + customHeaders, + headerName = 'authorization', + etag, + isPost, +}: { + clientKey: string; + connectionId: string; + appName: string; + customHeaders?: Record; + headerName?: string; + etag?: string; + isPost?: boolean; +}): Record => { + const headers: Record = { + accept: 'application/json', + [headerName.toLocaleLowerCase()]: clientKey, + 'unleash-sdk': sdkVersion, + 'unleash-appname': appName, + }; + + if (isPost) { + headers['content-type'] = 'application/json'; + } + + if (etag) { + headers['if-none-match'] = etag; + } + + Object.entries(customHeaders || {}) + .filter(notNullOrUndefined) + .forEach( + ([name, value]) => (headers[name.toLocaleLowerCase()] = value) + ); + + headers['unleash-connection-id'] = connectionId; + + return headers; +}; diff --git a/src/version.ts b/src/version.ts index 896c627..de28b7c 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const sdkVersion = `unleash-js@__VERSION__`; +export const sdkVersion = `unleash-client-js:__VERSION__`; From f69227e9cc4cb32aefae50851976f0b307176b26 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Fri, 31 Jan 2025 09:16:19 +0000 Subject: [PATCH 07/11] v3.7.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 40ea932..0a88bfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unleash-proxy-client", - "version": "3.7.2", + "version": "3.7.3", "description": "A browser client that can be used together with Unleash Edge or the Unleash Frontend API.", "type": "module", "main": "./build/index.cjs", From 491fe5956f9a68288e2291329657f4cff7312387 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:57:17 +0100 Subject: [PATCH 08/11] fix: unstable async bootstrap initialization (#244) --- src/index.test.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 6 ++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index e28529b..68a8485 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -317,6 +317,51 @@ test('Should bootstrap data when bootstrap is provided', async () => { expect(localStorage.getItem(storeKey)).toBe(JSON.stringify(bootstrap)); }); +it('should return correct variant if called asynchronously multiple times', async () => { + const bootstrap = [ + { + name: 'foo', + enabled: true, + variant: { + name: 'A', + enabled: true, + payload: { + type: 'string', + value: 'FOO', + }, + }, + impressionData: false, + }, + ]; + + const config: IConfig = { + url: 'http://localhost/test', + clientKey: '12', + appName: 'web', + refreshInterval: 0, + metricsInterval: 0, + disableRefresh: true, + bootstrapOverride: true, + bootstrap, + createAbortController: () => null, + }; + const client = new UnleashClient(config); + + for (let i = 0; i < 12; i++) { + await true; + + expect(client.getVariant('foo')).toEqual({ + name: 'A', + enabled: true, + feature_enabled: true, + payload: { + type: 'string', + value: 'FOO', + }, + }); + } +}); + test('Should set internal toggle state when bootstrap is set, before client is started', async () => { localStorage.clear(); const storeKey = 'unleash:repository:repo'; diff --git a/src/index.ts b/src/index.ts index 9f29185..f901a99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -395,12 +395,12 @@ export class UnleashClient extends TinyEmitter { const sessionId = await this.resolveSessionId(); this.context = { sessionId, ...this.context }; - this.toggles = (await this.storage.get(storeKey)) || []; + const storedToggles = (await this.storage.get(storeKey)) || []; this.lastRefreshTimestamp = await this.getLastRefreshTimestamp(); if ( this.bootstrap && - (this.bootstrapOverride || this.toggles.length === 0) + (this.bootstrapOverride || storedToggles.length === 0) ) { await this.storage.save(storeKey, this.bootstrap); this.toggles = this.bootstrap; @@ -410,6 +410,8 @@ export class UnleashClient extends TinyEmitter { await this.storeLastRefreshTimestamp(); this.setReady(); + } else { + this.toggles = storedToggles; } this.sdkState = 'healthy'; From 738a444b9ea4b1d054adc1822a1f314f26f204e4 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Wed, 26 Mar 2025 10:11:53 +0000 Subject: [PATCH 09/11] v3.7.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a88bfd..86bffd1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unleash-proxy-client", - "version": "3.7.3", + "version": "3.7.4", "description": "A browser client that can be used together with Unleash Edge or the Unleash Frontend API.", "type": "module", "main": "./build/index.cjs", From 6d57a98f33cae432c31d5901465f69e86b7a07b5 Mon Sep 17 00:00:00 2001 From: isawalhi <55400143+isawalhi@users.noreply.github.com> Date: Tue, 8 Apr 2025 12:17:35 +0400 Subject: [PATCH 10/11] fix(metrics): use clearInterval instead of clearTimeout in stop() (#246) --- src/metrics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/metrics.ts b/src/metrics.ts index 0ef8c55..01d40d0 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -107,7 +107,7 @@ export default class Metrics { public stop() { if (this.timer) { - clearTimeout(this.timer); + clearInterval(this.timer); delete this.timer; } } From 3c994f09c00ea0fe03cbba6f8d3cf21864f6aafc Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Tue, 8 Apr 2025 08:24:22 +0000 Subject: [PATCH 11/11] v3.7.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86bffd1..1b27973 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "unleash-proxy-client", - "version": "3.7.4", + "version": "3.7.5", "description": "A browser client that can be used together with Unleash Edge or the Unleash Frontend API.", "type": "module", "main": "./build/index.cjs",