From 244f791273abf5dca9b704a5467e3316994da094 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 7 Jan 2025 15:01:36 +0100 Subject: [PATCH 01/10] Chore: Remove reference to Unleash Proxy in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 53491f8..be43290 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "unleash-proxy-client", "version": "3.6.1", - "description": "A browser client that can be used together with the unleash-proxy.", + "description": "A browser client that can be used together with Unleash Edge or the Unleash Frontend API.", "type": "module", "main": "./build/index.cjs", "types": "./build/cjs/index.d.ts", From 6e42aa99dfbb79e0b01a2bc4d83c6606e6f7fa39 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 7 Jan 2025 15:09:58 +0100 Subject: [PATCH 02/10] feat(1-3223): add test for client identification headers --- src/index.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 7ea1cf4..514d3d4 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1356,6 +1356,28 @@ test('Should pass custom headers', async () => { }); }); +test('Should add X-UNLEASH headers', async () => { + fetchMock.mockResponses([JSON.stringify(data), { status: 200 }]); + const config: IConfig = { + url: 'http://localhost/test', + clientKey: 'some123key', + appName: 'web', + }; + const client = new UnleashClient(config); + await client.start(); + + const request = getTypeSafeRequest(fetchMock); + + const uuidFormat = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + + expect(request.headers).toMatchObject({ + 'x-unleash-sdk': 'unleash-js@1.0.0', + 'x-unleash-connection-id': expect.stringMatching(uuidFormat), + 'x-unleash-appname': 'web', + }); +}); + test('Should emit impression events on getVariant calls when impressionData is true', (done) => { const bootstrap = [ { From c0523cc8f1fa6c8b478c6b0f125e82f219baf6cd Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 7 Jan 2025 15:17:21 +0100 Subject: [PATCH 03/10] feat(1-3223): add client identification headers --- src/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/index.ts b/src/index.ts index fc67a01..e3bb8c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ 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'; @@ -169,6 +170,8 @@ export class UnleashClient extends TinyEmitter { private lastError: any; private experimental: IExperimentalConfig; private lastRefreshTimestamp: number; + private connectionId: string; + private sdkVersion: string; constructor({ storageProvider, @@ -274,6 +277,9 @@ export class UnleashClient extends TinyEmitter { customHeaders, metricsIntervalInitial, }); + + this.connectionId = uuidv4(); + this.sdkVersion = '1.0.0'; } public getAllToggles(): IToggle[] { @@ -468,6 +474,9 @@ export class UnleashClient extends TinyEmitter { const headers = { [this.headerName]: this.clientKey, Accept: 'application/json', + 'x-unleash-sdk': `unleash-js@${this.sdkVersion}`, + 'x-unleash-connection-id': this.connectionId, + 'x-unleash-appname': this.context.appName, }; if (isPOST) { headers['Content-Type'] = 'application/json'; From fc2a85120b303f8f57c06fd6be47479a6ada381b Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 7 Jan 2025 15:31:45 +0100 Subject: [PATCH 04/10] feat(1-3223): use sdk version from package.json --- src/index.test.ts | 3 ++- src/index.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 514d3d4..21b9783 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,4 +1,5 @@ import { FetchMock } from 'jest-fetch-mock'; +import { version as sdkVersion } from '../package.json'; import 'jest-localstorage-mock'; import * as data from './test/testdata.json'; import IStorageProvider from './storage-provider'; @@ -1372,7 +1373,7 @@ 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; expect(request.headers).toMatchObject({ - 'x-unleash-sdk': 'unleash-js@1.0.0', + 'x-unleash-sdk': `unleash-js@${sdkVersion}`, 'x-unleash-connection-id': expect.stringMatching(uuidFormat), 'x-unleash-appname': 'web', }); diff --git a/src/index.ts b/src/index.ts index e3bb8c5..6bce75f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { TinyEmitter } from 'tiny-emitter'; +import { version as sdkVersion } from '../package.json'; import { v4 as uuidv4 } from 'uuid'; import Metrics from './metrics'; import type IStorageProvider from './storage-provider'; @@ -279,7 +280,7 @@ export class UnleashClient extends TinyEmitter { }); this.connectionId = uuidv4(); - this.sdkVersion = '1.0.0'; + this.sdkVersion = sdkVersion; } public getAllToggles(): IToggle[] { From ab3be92f3450a9248d55ea666f5e887faccad178 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 7 Jan 2025 15:57:12 +0100 Subject: [PATCH 05/10] feat(1-3223): also add headers for metrics --- src/index.test.ts | 30 ++++++++++++++++++++++++------ src/index.ts | 8 +++++--- src/metrics.ts | 11 +++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 21b9783..03c675d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1358,25 +1358,43 @@ test('Should pass custom headers', async () => { }); test('Should add X-UNLEASH headers', async () => { - fetchMock.mockResponses([JSON.stringify(data), { status: 200 }]); + fetchMock.mockResponses( + [JSON.stringify(data), { status: 200 }], + [JSON.stringify(data), { status: 200 }] + ); + const appName = 'unleash-client-test'; const config: IConfig = { url: 'http://localhost/test', clientKey: 'some123key', - appName: 'web', + appName, }; const client = new UnleashClient(config); await client.start(); - const request = getTypeSafeRequest(fetchMock); + const featureRequest = getTypeSafeRequest(fetchMock, 0); + + client.isEnabled('count-metrics'); + jest.advanceTimersByTime(2001); + + const metricsRequest = getTypeSafeRequest(fetchMock, 1); const uuidFormat = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - expect(request.headers).toMatchObject({ + const expectedHeaders = { 'x-unleash-sdk': `unleash-js@${sdkVersion}`, 'x-unleash-connection-id': expect.stringMatching(uuidFormat), - 'x-unleash-appname': 'web', - }); + 'x-unleash-appname': appName, + }; + + const getConnectionId = (request: any) => + JSON.parse(JSON.stringify(request.headers))['x-unleash-connection-id']; + + expect(featureRequest.headers).toMatchObject(expectedHeaders); + expect(metricsRequest.headers).toMatchObject(expectedHeaders); + expect(getConnectionId(featureRequest)).toEqual( + getConnectionId(metricsRequest) + ); }); test('Should emit impression events on getVariant calls when impressionData is true', (done) => { diff --git a/src/index.ts b/src/index.ts index 6bce75f..4cdc7d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -265,6 +265,9 @@ export class UnleashClient extends TinyEmitter { bootstrap && bootstrap.length > 0 ? bootstrap : undefined; this.bootstrapOverride = bootstrapOverride; + this.connectionId = uuidv4(); + this.sdkVersion = sdkVersion; + this.metrics = new Metrics({ onError: this.emit.bind(this, EVENTS.ERROR), onSent: this.emit.bind(this, EVENTS.SENT), @@ -277,10 +280,9 @@ export class UnleashClient extends TinyEmitter { headerName, customHeaders, metricsIntervalInitial, + connectionId: this.connectionId, + sdkVersion, }); - - this.connectionId = uuidv4(); - this.sdkVersion = sdkVersion; } public getAllToggles(): IToggle[] { diff --git a/src/metrics.ts b/src/metrics.ts index 77abcbe..e73847d 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -14,6 +14,8 @@ export interface MetricsOptions { headerName: string; customHeaders?: Record; metricsIntervalInitial: number; + connectionId: string; + sdkVersion: string; } interface VariantBucket { @@ -53,6 +55,8 @@ export default class Metrics { private headerName: string; private customHeaders: Record; private metricsIntervalInitial: number; + private connectionId: string; + private sdkVersion: string; constructor({ onError, @@ -66,6 +70,8 @@ export default class Metrics { headerName, customHeaders = {}, metricsIntervalInitial, + connectionId, + sdkVersion, }: MetricsOptions) { this.onError = onError; this.onSent = onSent || doNothing; @@ -79,6 +85,8 @@ export default class Metrics { this.fetch = fetch; this.headerName = headerName; this.customHeaders = customHeaders; + this.connectionId = connectionId; + this.sdkVersion = sdkVersion; } public start() { @@ -121,6 +129,9 @@ export default class Metrics { [this.headerName]: this.clientKey, Accept: 'application/json', 'Content-Type': 'application/json', + 'x-unleash-sdk': `unleash-js@${this.sdkVersion}`, + 'x-unleash-connection-id': this.connectionId, + 'x-unleash-appname': this.appName, }; Object.entries(this.customHeaders) From e8858bfcd0fd78fb88d2928e8829d46955cfd31b Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 8 Jan 2025 08:41:32 +0100 Subject: [PATCH 06/10] feat(1-3223): lowercase x-unleash in test name --- src/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.test.ts b/src/index.test.ts index 03c675d..52f15ad 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1357,7 +1357,7 @@ test('Should pass custom headers', async () => { }); }); -test('Should add X-UNLEASH headers', async () => { +test('Should add `x-unleash` headers', async () => { fetchMock.mockResponses( [JSON.stringify(data), { status: 200 }], [JSON.stringify(data), { status: 200 }] From e21c790c616ad5128b67578067f98e5e712ba3c1 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 8 Jan 2025 08:41:48 +0100 Subject: [PATCH 07/10] feat(1-3223): try different import syntax + use version directly --- src/index.ts | 8 +++----- src/metrics.ts | 7 ++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4cdc7d3..7ceb7c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ import { TinyEmitter } from 'tiny-emitter'; -import { version as sdkVersion } from '../package.json'; import { v4 as uuidv4 } from 'uuid'; import Metrics from './metrics'; import type IStorageProvider from './storage-provider'; @@ -12,6 +11,8 @@ import { urlWithContextAsQuery, } from './util'; +import packageJSON = require('../package.json'); + const DEFINED_FIELDS = [ 'userId', 'sessionId', @@ -172,7 +173,6 @@ export class UnleashClient extends TinyEmitter { private experimental: IExperimentalConfig; private lastRefreshTimestamp: number; private connectionId: string; - private sdkVersion: string; constructor({ storageProvider, @@ -266,7 +266,6 @@ export class UnleashClient extends TinyEmitter { this.bootstrapOverride = bootstrapOverride; this.connectionId = uuidv4(); - this.sdkVersion = sdkVersion; this.metrics = new Metrics({ onError: this.emit.bind(this, EVENTS.ERROR), @@ -281,7 +280,6 @@ export class UnleashClient extends TinyEmitter { customHeaders, metricsIntervalInitial, connectionId: this.connectionId, - sdkVersion, }); } @@ -477,7 +475,7 @@ export class UnleashClient extends TinyEmitter { const headers = { [this.headerName]: this.clientKey, Accept: 'application/json', - 'x-unleash-sdk': `unleash-js@${this.sdkVersion}`, + 'x-unleash-sdk': `unleash-js@${packageJSON.version}`, 'x-unleash-connection-id': this.connectionId, 'x-unleash-appname': this.context.appName, }; diff --git a/src/metrics.ts b/src/metrics.ts index e73847d..3523ae8 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,6 +1,7 @@ // Simplified version of: https://github.com/Unleash/unleash-client-node/blob/main/src/metrics.ts import { notNullOrUndefined } from './util'; +import packageJSON = require('../package.json'); export interface MetricsOptions { onError: OnError; @@ -15,7 +16,6 @@ export interface MetricsOptions { customHeaders?: Record; metricsIntervalInitial: number; connectionId: string; - sdkVersion: string; } interface VariantBucket { @@ -56,7 +56,6 @@ export default class Metrics { private customHeaders: Record; private metricsIntervalInitial: number; private connectionId: string; - private sdkVersion: string; constructor({ onError, @@ -71,7 +70,6 @@ export default class Metrics { customHeaders = {}, metricsIntervalInitial, connectionId, - sdkVersion, }: MetricsOptions) { this.onError = onError; this.onSent = onSent || doNothing; @@ -86,7 +84,6 @@ export default class Metrics { this.headerName = headerName; this.customHeaders = customHeaders; this.connectionId = connectionId; - this.sdkVersion = sdkVersion; } public start() { @@ -129,7 +126,7 @@ export default class Metrics { [this.headerName]: this.clientKey, Accept: 'application/json', 'Content-Type': 'application/json', - 'x-unleash-sdk': `unleash-js@${this.sdkVersion}`, + 'x-unleash-sdk': `unleash-js@${packageJSON.version}`, 'x-unleash-connection-id': this.connectionId, 'x-unleash-appname': this.appName, }; From 57081a108f96e7766df7584864463017d316e737 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 8 Jan 2025 08:46:16 +0100 Subject: [PATCH 08/10] feat(1-3223): use const import instead --- src/index.test.ts | 5 +++-- src/index.ts | 3 ++- src/metrics.ts | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 52f15ad..9a7841c 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,5 +1,6 @@ import { FetchMock } from 'jest-fetch-mock'; -import { version as sdkVersion } from '../package.json'; +// 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'; @@ -1382,7 +1383,7 @@ 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@${sdkVersion}`, + 'x-unleash-sdk': `unleash-js@${packageJSON.version}`, 'x-unleash-connection-id': expect.stringMatching(uuidFormat), 'x-unleash-appname': appName, }; diff --git a/src/index.ts b/src/index.ts index 7ceb7c1..7e82219 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,8 @@ import { urlWithContextAsQuery, } from './util'; -import packageJSON = require('../package.json'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageJSON = require('../package.json'); const DEFINED_FIELDS = [ 'userId', diff --git a/src/metrics.ts b/src/metrics.ts index 3523ae8..29e2c9f 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -1,7 +1,8 @@ // Simplified version of: https://github.com/Unleash/unleash-client-node/blob/main/src/metrics.ts import { notNullOrUndefined } from './util'; -import packageJSON = require('../package.json'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageJSON = require('../package.json'); export interface MetricsOptions { onError: OnError; From 42fcb45d0216e4594fc0745ca05176724c21f0eb Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 8 Jan 2025 08:49:01 +0100 Subject: [PATCH 09/10] feat(1-3223): simplify access --- src/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.test.ts b/src/index.test.ts index 9a7841c..8c4ef3c 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1389,7 +1389,7 @@ test('Should add `x-unleash` headers', async () => { }; const getConnectionId = (request: any) => - JSON.parse(JSON.stringify(request.headers))['x-unleash-connection-id']; + request.headers['x-unleash-connection-id']; expect(featureRequest.headers).toMatchObject(expectedHeaders); expect(metricsRequest.headers).toMatchObject(expectedHeaders); From f4bece36ecbd246b1fd5d7f3b8b0ec434655a998 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 8 Jan 2025 08:59:09 +0100 Subject: [PATCH 10/10] feat(1-3223): add connection id to test constructors --- src/metrics.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/metrics.test.ts b/src/metrics.test.ts index d460c73..a2f9dc6 100644 --- a/src/metrics.test.ts +++ b/src/metrics.test.ts @@ -22,6 +22,7 @@ test('should be disabled by flag disableMetrics', async () => { fetch: fetchMock, headerName: 'Authorization', metricsIntervalInitial: 0, + connectionId: '123', }); metrics.count('foo', true); @@ -42,6 +43,7 @@ test('should send metrics', async () => { fetch: fetchMock, headerName: 'Authorization', metricsIntervalInitial: 0, + connectionId: '123', }); metrics.count('foo', true); @@ -79,6 +81,7 @@ test('should send metrics with custom auth header', async () => { fetch: fetchMock, headerName: 'NotAuthorization', metricsIntervalInitial: 0, + connectionId: '123', }); metrics.count('foo', true); @@ -103,6 +106,7 @@ test('Should send initial metrics after 2 seconds', () => { fetch: fetchMock, headerName: 'Authorization', metricsIntervalInitial: 2, + connectionId: '123', }); metrics.start(); @@ -127,6 +131,7 @@ test('Should send initial metrics after 20 seconds, when metricsIntervalInitial fetch: fetchMock, headerName: 'Authorization', metricsIntervalInitial: 20, + connectionId: '123', }); metrics.start(); @@ -151,6 +156,7 @@ test('Should send metrics for initial and after metrics interval', () => { fetch: fetchMock, headerName: 'Authorization', metricsIntervalInitial: 2, + connectionId: '123', }); metrics.start(); @@ -178,6 +184,7 @@ test('Should not send initial metrics if disabled', () => { fetch: fetchMock, headerName: 'Authorization', metricsIntervalInitial: 0, + connectionId: '123', }); metrics.start(); @@ -202,6 +209,7 @@ test('should send metrics based on timer interval', async () => { fetch: fetchMock, headerName: 'Authorization', metricsIntervalInitial: 2, + connectionId: '123', }); metrics.start(); @@ -242,6 +250,7 @@ describe('Custom headers for metrics', () => { fetch: fetchMock, headerName: 'Authorization', customHeaders, + connectionId: '123', metricsIntervalInitial: 2, });