From 53af412dd244c202ea16d3ba1bf7d99d2c511ae0 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:31:48 -0700 Subject: [PATCH 01/25] feat: Implement support for browser requests. --- packages/sdk/browser/src/platform/Backoff.ts | 27 ++++++ .../src/platform/BrowserEventSourceShim.ts | 91 +++++++++++++++++++ .../browser/src/platform/BrowserPlatform.ts | 4 +- .../browser/src/platform/BrowserRequests.ts | 18 ++++ .../src/platform/PlatformRequests.ts | 9 ++ .../server-node/src/platform/NodeRequests.ts | 9 ++ .../common/src/api/platform/Requests.ts | 20 ++++ 7 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 packages/sdk/browser/src/platform/Backoff.ts create mode 100644 packages/sdk/browser/src/platform/BrowserEventSourceShim.ts create mode 100644 packages/sdk/browser/src/platform/BrowserRequests.ts diff --git a/packages/sdk/browser/src/platform/Backoff.ts b/packages/sdk/browser/src/platform/Backoff.ts new file mode 100644 index 0000000000..30d272e128 --- /dev/null +++ b/packages/sdk/browser/src/platform/Backoff.ts @@ -0,0 +1,27 @@ +const maxRetryDelay = 30 * 1000; // Maximum retry delay 30 seconds. +const jitterRatio = 0.5; // Delay should be 50%-100% of calculated time. + +function jitter(computedDelayMillis: number): number { + return computedDelayMillis - Math.trunc(Math.random() * jitterRatio * computedDelayMillis); +} + +export default class Backoff { + private retryCount: number = 0; + + constructor(private readonly initialRetryDelayMillis: number) {} + + reset(): void { + this.retryCount = 0; + } + + backoff(): number { + const delay = this.initialRetryDelayMillis * 2 ** this.retryCount; + return delay > maxRetryDelay ? maxRetryDelay : delay; + } + + getNextRetryDelay(): number { + const delay = jitter(this.backoff()); + this.retryCount += 1; + return delay; + } +} diff --git a/packages/sdk/browser/src/platform/BrowserEventSourceShim.ts b/packages/sdk/browser/src/platform/BrowserEventSourceShim.ts new file mode 100644 index 0000000000..c479a34e11 --- /dev/null +++ b/packages/sdk/browser/src/platform/BrowserEventSourceShim.ts @@ -0,0 +1,91 @@ +import { + EventListener, + EventName, + EventSourceInitDict, + HttpErrorResponse, + EventSource as LDEventSource, +} from '@launchdarkly/js-client-sdk-common'; + +import Backoff from './Backoff'; + +/** + * Implementation Notes: + * + * This event source does not support a read-timeout. + * This event source does not support customized verbs. + * This event source does not support headers. + */ + +export default class BrowserEventSourceShim implements LDEventSource { + private es?: EventSource; + private backoff: Backoff; + private errorFilter: (err: HttpErrorResponse) => boolean; + + // The type of the handle can be platform specific and we treat is opaquely. + private reconnectTimeoutHandle?: any; + + constructor( + private readonly url: string, + options: EventSourceInitDict, + ) { + this.backoff = new Backoff(options.initialRetryDelayMillis); + this.errorFilter = options.errorFilter; + this.createEventSource(); + } + + onclose: (() => void) | undefined; + + onerror: ((err?: HttpErrorResponse) => void) | undefined; + + onopen: (() => void) | undefined; + + onretrying: ((e: { delayMillis: number }) => void) | undefined; + + private createEventSource() { + this.es = new EventSource(this.url); + this.es.onopen = () => { + this.backoff.reset(); + this.onopen?.(); + }; + // The error could be from a polyfill, or from the browser event source, so we are loose on the + // typing. + this.es.onerror = (err: any) => { + this.handleError(err); + }; + } + + addEventListener(type: EventName, listener: EventListener): void { + // TODO: Cache listeners so they can be re-added. + this.es?.addEventListener(type, listener); + } + + close(): void { + // Ensure any pending retry attempts are not done. + clearTimeout(this.reconnectTimeoutHandle); + this.reconnectTimeoutHandle = undefined; + this.es?.close(); + } + + private tryConnect(delayMs: number) { + this.reconnectTimeoutHandle = setTimeout(() => { + this.createEventSource(); + }, delayMs); + } + + private handleError(err: any): void { + // The event source may not produce a status. But the LaunchDarkly + // polyfill can. If we can get the status, then we should stop retrying + // on certain error codes. + if (err.status && typeof err.status === 'number' && !this.errorFilter(err)) { + // If we encounter an unrecoverable condition, then we do not want to + // retry anymore. + this.close(); + return; + } + + const delay = this.backoff.getNextRetryDelay(); + + this.close(); + this.tryConnect(delay); + } +} diff --git a/packages/sdk/browser/src/platform/BrowserPlatform.ts b/packages/sdk/browser/src/platform/BrowserPlatform.ts index 8865ebc3c8..dc9d7054d9 100644 --- a/packages/sdk/browser/src/platform/BrowserPlatform.ts +++ b/packages/sdk/browser/src/platform/BrowserPlatform.ts @@ -2,10 +2,12 @@ import { Crypto, /* platform */ LDOptions, + Requests, Storage, } from '@launchdarkly/js-client-sdk-common'; import BrowserCrypto from './BrowserCrypto'; +import BrowserRequests from './BrowserRequests'; import LocalStorage, { isLocalStorageSupported } from './LocalStorage'; export default class BrowserPlatform /* implements platform.Platform */ { @@ -13,7 +15,7 @@ export default class BrowserPlatform /* implements platform.Platform */ { // info: Info; // fileSystem?: Filesystem; crypto: Crypto = new BrowserCrypto(); - // requests: Requests; + requests: Requests = new BrowserRequests(); storage?: Storage; constructor(options: LDOptions) { diff --git a/packages/sdk/browser/src/platform/BrowserRequests.ts b/packages/sdk/browser/src/platform/BrowserRequests.ts new file mode 100644 index 0000000000..917b6ac4aa --- /dev/null +++ b/packages/sdk/browser/src/platform/BrowserRequests.ts @@ -0,0 +1,18 @@ +import { + EventSourceInitDict, + EventSource as LDEventSource, + Options, + Requests, + Response, +} from '@launchdarkly/js-client-sdk-common'; + +import BrowserEventSourceShim from './BrowserEventSourceShim'; + +export default class BrowserRequests implements Requests { + fetch(url: string, options?: Options): Promise { + return this.fetch(url, options); + } + createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): LDEventSource { + return new BrowserEventSourceShim(url, eventSourceInitDict); + } +} diff --git a/packages/sdk/react-native/src/platform/PlatformRequests.ts b/packages/sdk/react-native/src/platform/PlatformRequests.ts index 5b345d2950..6651a7bb1c 100644 --- a/packages/sdk/react-native/src/platform/PlatformRequests.ts +++ b/packages/sdk/react-native/src/platform/PlatformRequests.ts @@ -1,6 +1,7 @@ import type { EventName, EventSource, + EventSourceCapabilities, EventSourceInitDict, LDLogger, Options, @@ -21,6 +22,14 @@ export default class PlatformRequests implements Requests { }); } + getEventSourceCapabilities(): EventSourceCapabilities { + return { + readTimeout: false, + headers: true, + customVerb: true, + }; + } + fetch(url: string, options?: Options): Promise { // @ts-ignore return fetch(url, options); diff --git a/packages/sdk/server-node/src/platform/NodeRequests.ts b/packages/sdk/server-node/src/platform/NodeRequests.ts index c951185195..3e83278b67 100644 --- a/packages/sdk/server-node/src/platform/NodeRequests.ts +++ b/packages/sdk/server-node/src/platform/NodeRequests.ts @@ -7,6 +7,7 @@ import { HttpsProxyAgentOptions } from 'https-proxy-agent'; import { EventSource as LDEventSource } from 'launchdarkly-eventsource'; import { + EventSourceCapabilities, LDLogger, LDProxyOptions, LDTLSOptions, @@ -158,6 +159,14 @@ export default class NodeRequests implements platform.Requests { return new LDEventSource(url, expandedOptions); } + getEventSourceCapabilities(): EventSourceCapabilities { + return { + readTimeout: true, + headers: true, + customVerb: true, + }; + } + usingProxy(): boolean { return this.hasProxy; } diff --git a/packages/shared/common/src/api/platform/Requests.ts b/packages/shared/common/src/api/platform/Requests.ts index 96168012b6..32ef8149e8 100644 --- a/packages/shared/common/src/api/platform/Requests.ts +++ b/packages/shared/common/src/api/platform/Requests.ts @@ -78,11 +78,31 @@ export interface Options { timeout?: number; } +export interface EventSourceCapabilities { + /** + * If true the event source supports read timeouts. + */ + readTimeout: boolean; + + /** + * If true the event source supports customized verbs POST/REPORT instead of + * only the default GET. + */ + customVerb: boolean; + + /** + * If true the event source supports setting HTTP headers. + */ + headers: boolean; +} + export interface Requests { fetch(url: string, options?: Options): Promise; createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource; + getEventSourceCapabilities(): EventSourceCapabilities; + /** * Returns true if a proxy is configured. */ From bf164dd8dc75bfc6e33ce28ca07b1ac64d6c3e43 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:46:26 -0700 Subject: [PATCH 02/25] feat: Add support for conditional event source capabilities. --- .../src/platform/PlatformRequests.ts | 9 +++++++++ .../server-node/src/platform/NodeRequests.ts | 9 +++++++++ .../common/src/api/platform/Requests.ts | 20 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/packages/sdk/react-native/src/platform/PlatformRequests.ts b/packages/sdk/react-native/src/platform/PlatformRequests.ts index 5b345d2950..6add1e7be3 100644 --- a/packages/sdk/react-native/src/platform/PlatformRequests.ts +++ b/packages/sdk/react-native/src/platform/PlatformRequests.ts @@ -1,6 +1,7 @@ import type { EventName, EventSource, + EventSourceCapabilities, EventSourceInitDict, LDLogger, Options, @@ -21,6 +22,14 @@ export default class PlatformRequests implements Requests { }); } + getEventSourceCapabilities(): EventSourceCapabilities { + return { + readTimeout: false, + headers: true, + customMethod: true, + }; + } + fetch(url: string, options?: Options): Promise { // @ts-ignore return fetch(url, options); diff --git a/packages/sdk/server-node/src/platform/NodeRequests.ts b/packages/sdk/server-node/src/platform/NodeRequests.ts index c951185195..ede410b63e 100644 --- a/packages/sdk/server-node/src/platform/NodeRequests.ts +++ b/packages/sdk/server-node/src/platform/NodeRequests.ts @@ -7,6 +7,7 @@ import { HttpsProxyAgentOptions } from 'https-proxy-agent'; import { EventSource as LDEventSource } from 'launchdarkly-eventsource'; import { + EventSourceCapabilities, LDLogger, LDProxyOptions, LDTLSOptions, @@ -158,6 +159,14 @@ export default class NodeRequests implements platform.Requests { return new LDEventSource(url, expandedOptions); } + getEventSourceCapabilities(): EventSourceCapabilities { + return { + readTimeout: true, + headers: true, + customMethod: true, + }; + } + usingProxy(): boolean { return this.hasProxy; } diff --git a/packages/shared/common/src/api/platform/Requests.ts b/packages/shared/common/src/api/platform/Requests.ts index 96168012b6..a771dcc454 100644 --- a/packages/shared/common/src/api/platform/Requests.ts +++ b/packages/shared/common/src/api/platform/Requests.ts @@ -78,11 +78,31 @@ export interface Options { timeout?: number; } +export interface EventSourceCapabilities { + /** + * If true the event source supports read timeouts. + */ + readTimeout: boolean; + + /** + * If true the event source supports customized verbs POST/REPORT instead of + * only the default GET. + */ + customMethod: boolean; + + /** + * If true the event source supports setting HTTP headers. + */ + headers: boolean; +} + export interface Requests { fetch(url: string, options?: Options): Promise; createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource; + getEventSourceCapabilities(): EventSourceCapabilities; + /** * Returns true if a proxy is configured. */ From 8307b032231bc054b312f7a4383a9d129a8bf76a Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:57:59 -0700 Subject: [PATCH 03/25] Add more comments. --- packages/shared/common/src/api/platform/Requests.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/shared/common/src/api/platform/Requests.ts b/packages/shared/common/src/api/platform/Requests.ts index a771dcc454..219d775f41 100644 --- a/packages/shared/common/src/api/platform/Requests.ts +++ b/packages/shared/common/src/api/platform/Requests.ts @@ -80,7 +80,14 @@ export interface Options { export interface EventSourceCapabilities { /** - * If true the event source supports read timeouts. + * If true the event source supports read timeouts. A read timeout for an + * event source represents the maximum time between receiving any data. + * If you receive 1 byte, and then a period of time greater than the read + * time out elapses, and you do not receive a second byte, then that would + * cause the event source to timeout. + * + * It is not a timeout for the read of the entire body, which should be + * indefinite. */ readTimeout: boolean; From c23bf45842403573711925ed0fc9eb11c4453948 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:03:53 -0700 Subject: [PATCH 04/25] Implement for edge SDKs. --- .../sdk/react-native/src/platform/PlatformRequests.ts | 4 ++-- .../akamai-edgeworker-sdk/src/platform/requests.ts | 9 +++++++++ packages/shared/sdk-server-edge/src/platform/requests.ts | 9 +++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/sdk/react-native/src/platform/PlatformRequests.ts b/packages/sdk/react-native/src/platform/PlatformRequests.ts index 6add1e7be3..ae5c3ab2c7 100644 --- a/packages/sdk/react-native/src/platform/PlatformRequests.ts +++ b/packages/sdk/react-native/src/platform/PlatformRequests.ts @@ -25,8 +25,8 @@ export default class PlatformRequests implements Requests { getEventSourceCapabilities(): EventSourceCapabilities { return { readTimeout: false, - headers: true, - customMethod: true, + headers: false, + customMethod: false, }; } diff --git a/packages/shared/akamai-edgeworker-sdk/src/platform/requests.ts b/packages/shared/akamai-edgeworker-sdk/src/platform/requests.ts index 5a6b728b00..453344e13c 100644 --- a/packages/shared/akamai-edgeworker-sdk/src/platform/requests.ts +++ b/packages/shared/akamai-edgeworker-sdk/src/platform/requests.ts @@ -6,6 +6,7 @@ import type { Options, Requests, Response, + EventSourceCapabilities, } from '@launchdarkly/js-server-sdk-common'; class NoopResponse implements Response { @@ -41,4 +42,12 @@ export default class EdgeRequests implements Requests { createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { return new NullEventSource(url, eventSourceInitDict); } + + getEventSourceCapabilities(): EventSourceCapabilities { + return { + readTimeout: false, + headers: false, + customMethod: false, + }; + } } diff --git a/packages/shared/sdk-server-edge/src/platform/requests.ts b/packages/shared/sdk-server-edge/src/platform/requests.ts index 2bc8010dc5..413bbb6b32 100644 --- a/packages/shared/sdk-server-edge/src/platform/requests.ts +++ b/packages/shared/sdk-server-edge/src/platform/requests.ts @@ -1,6 +1,7 @@ import { NullEventSource } from '@launchdarkly/js-server-sdk-common'; import type { EventSource, + EventSourceCapabilities, EventSourceInitDict, Options, Requests, @@ -16,4 +17,12 @@ export default class EdgeRequests implements Requests { createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): EventSource { return new NullEventSource(url, eventSourceInitDict); } + + getEventSourceCapabilities(): EventSourceCapabilities { + return { + readTimeout: false, + headers: false, + customMethod: false, + }; + } } From a084b9a5d1d3602acedbe665b446a46b1129d29a Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:20:54 -0700 Subject: [PATCH 05/25] Lint --- packages/sdk/react-native/src/platform/PlatformRequests.ts | 4 ++-- .../shared/akamai-edgeworker-sdk/src/platform/requests.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk/react-native/src/platform/PlatformRequests.ts b/packages/sdk/react-native/src/platform/PlatformRequests.ts index ae5c3ab2c7..6add1e7be3 100644 --- a/packages/sdk/react-native/src/platform/PlatformRequests.ts +++ b/packages/sdk/react-native/src/platform/PlatformRequests.ts @@ -25,8 +25,8 @@ export default class PlatformRequests implements Requests { getEventSourceCapabilities(): EventSourceCapabilities { return { readTimeout: false, - headers: false, - customMethod: false, + headers: true, + customMethod: true, }; } diff --git a/packages/shared/akamai-edgeworker-sdk/src/platform/requests.ts b/packages/shared/akamai-edgeworker-sdk/src/platform/requests.ts index 453344e13c..69cbeeb5e6 100644 --- a/packages/shared/akamai-edgeworker-sdk/src/platform/requests.ts +++ b/packages/shared/akamai-edgeworker-sdk/src/platform/requests.ts @@ -2,11 +2,11 @@ import { Headers, NullEventSource } from '@launchdarkly/js-server-sdk-common'; import type { EventSource, + EventSourceCapabilities, EventSourceInitDict, Options, Requests, Response, - EventSourceCapabilities, } from '@launchdarkly/js-server-sdk-common'; class NoopResponse implements Response { From 6acc5f47aa8c4bb2f272bb8afce024a22cf79272 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:23:05 -0700 Subject: [PATCH 06/25] Add getEventSourceCapabilities to mocks. --- packages/shared/mocks/src/platform.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared/mocks/src/platform.ts b/packages/shared/mocks/src/platform.ts index ef138ebd56..e3b0b9ecc5 100644 --- a/packages/shared/mocks/src/platform.ts +++ b/packages/shared/mocks/src/platform.ts @@ -49,6 +49,7 @@ export const createBasicPlatform = () => ({ requests: { fetch: jest.fn(), createEventSource: jest.fn(), + getEventSourceCapabilities: jest.fn(), }, storage: { get: jest.fn(), From b89e14e5eac45bd14825d7891357ce27d9c585a7 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:26:45 -0700 Subject: [PATCH 07/25] Test updates. --- ...{PollingProcessot.test.ts => PollingProcessor.test.ts} | 8 ++++++++ 1 file changed, 8 insertions(+) rename packages/shared/sdk-client/__tests__/polling/{PollingProcessot.test.ts => PollingProcessor.test.ts} (98%) diff --git a/packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts similarity index 98% rename from packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts rename to packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts index b6a98ae3eb..33cfa40771 100644 --- a/packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts +++ b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts @@ -2,6 +2,7 @@ import { waitFor } from '@testing-library/dom'; import { EventSource, + EventSourceCapabilities, EventSourceInitDict, Info, PlatformData, @@ -45,6 +46,13 @@ function makeRequests(): Requests { createEventSource(_url: string, _eventSourceInitDict: EventSourceInitDict): EventSource { throw new Error('Function not implemented.'); }, + getEventSourceCapabilities(): EventSourceCapabilities { + return { + readTimeout: false, + headers: false, + customMethod: false, + }; + }, }; } From 5becb5b410b5366c7ab82d9dda51775c9a4a990c Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:27:57 -0700 Subject: [PATCH 08/25] More test updates. --- .../sdk/react-native/__tests__/ReactNativeLDClient.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/sdk/react-native/__tests__/ReactNativeLDClient.test.ts b/packages/sdk/react-native/__tests__/ReactNativeLDClient.test.ts index 7a4f594c68..56ddebcbfc 100644 --- a/packages/sdk/react-native/__tests__/ReactNativeLDClient.test.ts +++ b/packages/sdk/react-native/__tests__/ReactNativeLDClient.test.ts @@ -42,6 +42,7 @@ jest.mock('../src/platform', () => ({ requests: { createEventSource: jest.fn(), fetch: jest.fn(), + getEventSourceCapabilities: jest.fn(), }, encoding: new PlatformEncoding(), storage: new PlatformStorage(logger), @@ -70,6 +71,7 @@ it('uses correct default diagnostic url', () => { requests: { createEventSource: jest.fn(), fetch: mockedFetch, + getEventSourceCapabilities: jest.fn(), }, encoding: new PlatformEncoding(), storage: new PlatformStorage(logger), @@ -97,6 +99,7 @@ it('uses correct default analytics event url', async () => { requests: { createEventSource: createMockEventSource, fetch: mockedFetch, + getEventSourceCapabilities: jest.fn(), }, encoding: new PlatformEncoding(), storage: new PlatformStorage(logger), @@ -128,6 +131,7 @@ it('uses correct default polling url', async () => { requests: { createEventSource: jest.fn(), fetch: mockedFetch, + getEventSourceCapabilities: jest.fn(), }, encoding: new PlatformEncoding(), storage: new PlatformStorage(logger), @@ -158,6 +162,7 @@ it('uses correct default streaming url', (done) => { requests: { createEventSource: mockedCreateEventSource, fetch: jest.fn(), + getEventSourceCapabilities: jest.fn(), }, encoding: new PlatformEncoding(), storage: new PlatformStorage(logger), @@ -197,6 +202,7 @@ it('includes authorization header for polling', async () => { requests: { createEventSource: jest.fn(), fetch: mockedFetch, + getEventSourceCapabilities: jest.fn(), }, encoding: new PlatformEncoding(), storage: new PlatformStorage(logger), @@ -231,6 +237,7 @@ it('includes authorization header for streaming', (done) => { requests: { createEventSource: mockedCreateEventSource, fetch: jest.fn(), + getEventSourceCapabilities: jest.fn(), }, encoding: new PlatformEncoding(), storage: new PlatformStorage(logger), From 8f63e45643ecdb78348b0a247547a287df24071d Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:30:23 -0700 Subject: [PATCH 09/25] More tests. --- .../sdk-client/__tests__/polling/PollingProcessor.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts index 33cfa40771..8e6a39c931 100644 --- a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts @@ -177,6 +177,13 @@ it('stops polling when stopped', (done) => { createEventSource(_url: string, _eventSourceInitDict: EventSourceInitDict): EventSource { throw new Error('Function not implemented.'); }, + getEventSourceCapabilities(): EventSourceCapabilities { + return { + readTimeout: false, + headers: false, + customMethod: false, + }; + }, }; const dataCallback = jest.fn(); const errorCallback = jest.fn(); From 1c5450f0a3d392584f1d2956b7632b6aa96f1efc Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:34:43 -0700 Subject: [PATCH 10/25] More test updates. --- .../sdk-client/__tests__/flag-manager/FlagPersistence.test.ts | 1 + .../shared/sdk-server/__tests__/data_sources/Requestor.test.ts | 3 +++ .../shared/sdk-server/__tests__/events/EventProcessor.test.ts | 3 +++ 3 files changed, 7 insertions(+) diff --git a/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts b/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts index 6e90c10af3..cd31ef46c4 100644 --- a/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts +++ b/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts @@ -329,6 +329,7 @@ function makeMockPlatform(storage: Storage, crypto: Crypto): Platform { requests: { fetch: jest.fn(), createEventSource: jest.fn(), + getEventSourceCapabilities: jest.fn(), }, }; } 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 1fb0558155..46a0640ac8 100644 --- a/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts +++ b/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts @@ -75,6 +75,9 @@ describe('given a requestor', () => { createEventSource(_url: string, _eventSourceInitDict: EventSourceInitDict): EventSource { throw new Error('Function not implemented.'); }, + getEventSourceCapabilities() { + throw new Error('Function not implemented.'); + }, }; requestor = new Requestor( diff --git a/packages/shared/sdk-server/__tests__/events/EventProcessor.test.ts b/packages/shared/sdk-server/__tests__/events/EventProcessor.test.ts index 8aed40555f..41fa4135ef 100644 --- a/packages/shared/sdk-server/__tests__/events/EventProcessor.test.ts +++ b/packages/shared/sdk-server/__tests__/events/EventProcessor.test.ts @@ -111,6 +111,9 @@ function makePlatform(requestState: RequestState) { createEventSource(_url: string, _eventSourceInitDict: EventSourceInitDict): EventSource { throw new Error('Function not implemented.'); }, + getEventSourceCapabilities() { + throw new Error('Function not implemented.'); + }, }; return { info, From f0a51c6f452a455a7b6a2468ee9fa91343995aa6 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:37:46 -0700 Subject: [PATCH 11/25] feat: Implement browser platform. --- .../src/platform/BrowserEventSourceShim.ts | 21 +++++++++++++++---- .../browser/src/platform/BrowserRequests.ts | 10 +++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/sdk/browser/src/platform/BrowserEventSourceShim.ts b/packages/sdk/browser/src/platform/BrowserEventSourceShim.ts index c479a34e11..b5e07b1a17 100644 --- a/packages/sdk/browser/src/platform/BrowserEventSourceShim.ts +++ b/packages/sdk/browser/src/platform/BrowserEventSourceShim.ts @@ -24,13 +24,15 @@ export default class BrowserEventSourceShim implements LDEventSource { // The type of the handle can be platform specific and we treat is opaquely. private reconnectTimeoutHandle?: any; + private listeners: Record = {}; + constructor( private readonly url: string, options: EventSourceInitDict, ) { this.backoff = new Backoff(options.initialRetryDelayMillis); this.errorFilter = options.errorFilter; - this.createEventSource(); + this.openConnection(); } onclose: (() => void) | undefined; @@ -41,7 +43,7 @@ export default class BrowserEventSourceShim implements LDEventSource { onretrying: ((e: { delayMillis: number }) => void) | undefined; - private createEventSource() { + private openConnection() { this.es = new EventSource(this.url); this.es.onopen = () => { this.backoff.reset(); @@ -51,11 +53,18 @@ export default class BrowserEventSourceShim implements LDEventSource { // typing. this.es.onerror = (err: any) => { this.handleError(err); + this.onerror?.(err); }; + Object.entries(this.listeners).forEach(([eventName, listeners]) => { + listeners.forEach((listener) => { + this.es?.addEventListener(eventName, listener); + }); + }); } addEventListener(type: EventName, listener: EventListener): void { - // TODO: Cache listeners so they can be re-added. + this.listeners[type] ??= []; + this.listeners[type].push(listener); this.es?.addEventListener(type, listener); } @@ -63,12 +72,16 @@ export default class BrowserEventSourceShim implements LDEventSource { // Ensure any pending retry attempts are not done. clearTimeout(this.reconnectTimeoutHandle); this.reconnectTimeoutHandle = undefined; + + // Close the event source and notify any listeners. this.es?.close(); + this.onclose?.(); } private tryConnect(delayMs: number) { + this.onretrying?.({ delayMillis: delayMs }); this.reconnectTimeoutHandle = setTimeout(() => { - this.createEventSource(); + this.openConnection(); }, delayMs); } diff --git a/packages/sdk/browser/src/platform/BrowserRequests.ts b/packages/sdk/browser/src/platform/BrowserRequests.ts index 917b6ac4aa..ca4d42f51d 100644 --- a/packages/sdk/browser/src/platform/BrowserRequests.ts +++ b/packages/sdk/browser/src/platform/BrowserRequests.ts @@ -1,4 +1,5 @@ import { + EventSourceCapabilities, EventSourceInitDict, EventSource as LDEventSource, Options, @@ -12,7 +13,16 @@ export default class BrowserRequests implements Requests { fetch(url: string, options?: Options): Promise { return this.fetch(url, options); } + createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): LDEventSource { return new BrowserEventSourceShim(url, eventSourceInitDict); } + + getEventSourceCapabilities(): EventSourceCapabilities { + return { + customMethod: false, + readTimeout: false, + headers: false, + }; + } } From d2597913b835b8c213df5e7b2323858fdae0a025 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:16:19 -0700 Subject: [PATCH 12/25] Rename browser event source. --- packages/sdk/browser/src/platform/BrowserRequests.ts | 4 ++-- ...ventSourceShim.ts => DefaultBrowserEventSource.ts} | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) rename packages/sdk/browser/src/platform/{BrowserEventSourceShim.ts => DefaultBrowserEventSource.ts} (93%) diff --git a/packages/sdk/browser/src/platform/BrowserRequests.ts b/packages/sdk/browser/src/platform/BrowserRequests.ts index ca4d42f51d..52d4d0dbd4 100644 --- a/packages/sdk/browser/src/platform/BrowserRequests.ts +++ b/packages/sdk/browser/src/platform/BrowserRequests.ts @@ -7,7 +7,7 @@ import { Response, } from '@launchdarkly/js-client-sdk-common'; -import BrowserEventSourceShim from './BrowserEventSourceShim'; +import DefaultBrowserEventSource from './DefaultBrowserEventSource'; export default class BrowserRequests implements Requests { fetch(url: string, options?: Options): Promise { @@ -15,7 +15,7 @@ export default class BrowserRequests implements Requests { } createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): LDEventSource { - return new BrowserEventSourceShim(url, eventSourceInitDict); + return new DefaultBrowserEventSource(url, eventSourceInitDict); } getEventSourceCapabilities(): EventSourceCapabilities { diff --git a/packages/sdk/browser/src/platform/BrowserEventSourceShim.ts b/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts similarity index 93% rename from packages/sdk/browser/src/platform/BrowserEventSourceShim.ts rename to packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts index b5e07b1a17..be791ad9b4 100644 --- a/packages/sdk/browser/src/platform/BrowserEventSourceShim.ts +++ b/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts @@ -16,7 +16,11 @@ import Backoff from './Backoff'; * This event source does not support headers. */ -export default class BrowserEventSourceShim implements LDEventSource { +/** + * Browser event source implementation which extends the built-in event + * source with additional reconnection logic. + */ +export default class DefaultBrowserEventSource implements LDEventSource { private es?: EventSource; private backoff: Backoff; private errorFilter: (err: HttpErrorResponse) => boolean; @@ -86,19 +90,18 @@ export default class BrowserEventSourceShim implements LDEventSource { } private handleError(err: any): void { + this.close(); + // The event source may not produce a status. But the LaunchDarkly // polyfill can. If we can get the status, then we should stop retrying // on certain error codes. if (err.status && typeof err.status === 'number' && !this.errorFilter(err)) { // If we encounter an unrecoverable condition, then we do not want to // retry anymore. - this.close(); return; } const delay = this.backoff.getNextRetryDelay(); - - this.close(); this.tryConnect(delay); } } From 679b813640bd096425abb5a6e229e6fda3daf2e1 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:04:42 -0700 Subject: [PATCH 13/25] Add backoff tests. --- .../__tests__/platform/Backoff.test.ts | 67 +++++++++++++++++ packages/sdk/browser/src/platform/Backoff.ts | 74 +++++++++++++++---- .../src/platform/DefaultBrowserEventSource.ts | 5 +- 3 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 packages/sdk/browser/__tests__/platform/Backoff.test.ts diff --git a/packages/sdk/browser/__tests__/platform/Backoff.test.ts b/packages/sdk/browser/__tests__/platform/Backoff.test.ts new file mode 100644 index 0000000000..3d32614ae8 --- /dev/null +++ b/packages/sdk/browser/__tests__/platform/Backoff.test.ts @@ -0,0 +1,67 @@ +import Backoff from '../../src/platform/Backoff'; + +const noJitter = (): number => 0; +const maxJitter = (): number => 1; + +it.each([1, 1000, 5000])('has the correct starting delay', (initialDelay) => { + const backoff = new Backoff(initialDelay, noJitter); + expect(backoff.fail()).toEqual(initialDelay); +}); + +it.each([1, 1000, 5000])('doubles delay on consecutive failures', (initialDelay) => { + const backoff = new Backoff(initialDelay, noJitter); + expect(backoff.fail()).toEqual(initialDelay); + expect(backoff.fail()).toEqual(initialDelay * 2); + expect(backoff.fail()).toEqual(initialDelay * 4); +}); + +it('stops increasing delay when the max backoff is encountered', () => { + const backoff = new Backoff(5000, noJitter); + expect(backoff.fail()).toEqual(5000); + expect(backoff.fail()).toEqual(10000); + expect(backoff.fail()).toEqual(20000); + expect(backoff.fail()).toEqual(30000); + + const backoff2 = new Backoff(1000, noJitter); + expect(backoff2.fail()).toEqual(1000); + expect(backoff2.fail()).toEqual(2000); + expect(backoff2.fail()).toEqual(4000); + expect(backoff2.fail()).toEqual(8000); + expect(backoff2.fail()).toEqual(16000); + expect(backoff2.fail()).toEqual(30000); +}); + +it('handles an initial retry delay longer than the maximum retry delay', () => { + const backoff = new Backoff(40000, noJitter); + expect(backoff.fail()).toEqual(30000); +}); + +it('jitters the backoff value', () => { + const backoff = new Backoff(1000, maxJitter); + expect(backoff.fail()).toEqual(500); + expect(backoff.fail()).toEqual(1000); + expect(backoff.fail()).toEqual(2000); + expect(backoff.fail()).toEqual(4000); + expect(backoff.fail()).toEqual(8000); + expect(backoff.fail()).toEqual(15000); +}); + +it('resets the delay when the last successful connection was connected greater than RESET_INTERVAL', () => { + const backoff = new Backoff(1000, noJitter); + expect(backoff.fail(1000)).toEqual(1000); + backoff.success(2000); + expect(backoff.fail(62001)).toEqual(1000); + expect(backoff.fail(62002)).toEqual(2000); + backoff.success(64002); + expect(backoff.fail(124003)).toEqual(1000); +}); + +it('does not reset the delay when the connection did not persist longer than the RESET_INTERVAL', () => { + const backoff = new Backoff(1000, noJitter); + expect(backoff.fail(1000)).toEqual(1000); + backoff.success(2000); + expect(backoff.fail(61000)).toEqual(2000); + expect(backoff.fail(120000)).toEqual(4000); + backoff.success(124000); + expect(backoff.fail(183000)).toEqual(8000); +}); diff --git a/packages/sdk/browser/src/platform/Backoff.ts b/packages/sdk/browser/src/platform/Backoff.ts index 30d272e128..59e28b5b82 100644 --- a/packages/sdk/browser/src/platform/Backoff.ts +++ b/packages/sdk/browser/src/platform/Backoff.ts @@ -1,26 +1,72 @@ -const maxRetryDelay = 30 * 1000; // Maximum retry delay 30 seconds. -const jitterRatio = 0.5; // Delay should be 50%-100% of calculated time. - -function jitter(computedDelayMillis: number): number { - return computedDelayMillis - Math.trunc(Math.random() * jitterRatio * computedDelayMillis); -} +const MAX_RETRY_DELAY = 30 * 1000; // Maximum retry delay 30 seconds. +const JITTER_RATIO = 0.5; // Delay should be 50%-100% of calculated time. +const RESET_INTERVAL = 60 * 1000; // Reset interval in seconds. +/** + * Implements exponential backoff and jitter. This class tracks successful connections and failures + * and produces a retry delay. + * + * It does not start any timers or directly control a connection. + * + * The backoff follows an exponential backoff scheme with 50% jitter starting at + * initialRetryDelayMillis and capping at MAX_RETRY_DELAY. If RESET_INTERVAL has elapsed after a + * success, without an intervening faulure, then the backoff is reset to initialRetryDelayMillis. + */ export default class Backoff { private retryCount: number = 0; + private activeSince?: number; + private initialRetryDelayMillis: number; + /** + * The exponent at which the backoff delay will exceed the maximum. + * Beyond this limit the backoff can be set to the max. + */ + private readonly maxExponent: number; - constructor(private readonly initialRetryDelayMillis: number) {} + constructor( + initialRetryDelayMillis: number, + private readonly random = Math.random, + ) { + // Initial retry delay cannot be 0. + this.initialRetryDelayMillis = Math.max(1, initialRetryDelayMillis); + this.maxExponent = Math.ceil(Math.log2(MAX_RETRY_DELAY / this.initialRetryDelayMillis)); + } + + private backoff(): number { + const exponent = Math.min(this.retryCount, this.maxExponent); + const delay = this.initialRetryDelayMillis * 2 ** exponent; + return Math.min(delay, MAX_RETRY_DELAY); + } - reset(): void { - this.retryCount = 0; + private jitter(computedDelayMillis: number): number { + return computedDelayMillis - Math.trunc(this.random() * JITTER_RATIO * computedDelayMillis); } - backoff(): number { - const delay = this.initialRetryDelayMillis * 2 ** this.retryCount; - return delay > maxRetryDelay ? maxRetryDelay : delay; + /** + * This function should be called when a connection attempt is successful. + * + * @param timeStampMs The time of the success. Used primarily for testing, when not provided + * the current time is used. + */ + success(timeStampMs: number = Date.now()): void { + this.activeSince = timeStampMs; } - getNextRetryDelay(): number { - const delay = jitter(this.backoff()); + /** + * This function should be called when a connection fails. It returns the a delay, in + * milliseconds, after which a reconnection attempt should be made. + * + * @param timeStampMs The time of the success. Used primarily for testing, when not provided + * the current time is used. + * @returns The delay before the next connection attempt. + */ + fail(timeStampMs: number = Date.now()): number { + // If the last successful connection was active for more than the RESET_INTERVAL, then we + // return to the initial retry delay. + if (this.activeSince !== undefined && timeStampMs - this.activeSince > RESET_INTERVAL) { + this.retryCount = 0; + } + this.activeSince = undefined; + const delay = this.jitter(this.backoff()); this.retryCount += 1; return delay; } diff --git a/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts b/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts index be791ad9b4..c560c86daf 100644 --- a/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts +++ b/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts @@ -50,7 +50,7 @@ export default class DefaultBrowserEventSource implements LDEventSource { private openConnection() { this.es = new EventSource(this.url); this.es.onopen = () => { - this.backoff.reset(); + this.backoff.success(); this.onopen?.(); }; // The error could be from a polyfill, or from the browser event source, so we are loose on the @@ -101,7 +101,6 @@ export default class DefaultBrowserEventSource implements LDEventSource { return; } - const delay = this.backoff.getNextRetryDelay(); - this.tryConnect(delay); + this.tryConnect(this.backoff.fail()); } } From 0434103da5fb5cf1f36097cbebfd87e5f0b6d3d3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:15:07 -0700 Subject: [PATCH 14/25] Enable encoding. --- packages/sdk/browser/src/platform/BrowserPlatform.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/sdk/browser/src/platform/BrowserPlatform.ts b/packages/sdk/browser/src/platform/BrowserPlatform.ts index dc9d7054d9..827566d6a3 100644 --- a/packages/sdk/browser/src/platform/BrowserPlatform.ts +++ b/packages/sdk/browser/src/platform/BrowserPlatform.ts @@ -1,5 +1,6 @@ import { Crypto, + Encoding, /* platform */ LDOptions, Requests, @@ -7,11 +8,12 @@ import { } from '@launchdarkly/js-client-sdk-common'; import BrowserCrypto from './BrowserCrypto'; +import BrowserEncoding from './BrowserEncoding'; import BrowserRequests from './BrowserRequests'; import LocalStorage, { isLocalStorageSupported } from './LocalStorage'; export default class BrowserPlatform /* implements platform.Platform */ { - // encoding?: Encoding; + encoding: Encoding = new BrowserEncoding(); // info: Info; // fileSystem?: Filesystem; crypto: Crypto = new BrowserCrypto(); From 7c66b7ba7b171e202e93858d6b1040062475334d Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:32:02 -0700 Subject: [PATCH 15/25] Move to rollup. --- packages/sdk/browser/package.json | 13 +++--- packages/sdk/browser/rollup.config.js | 61 +++++++++++++++++++++++++++ packages/sdk/browser/src/index.ts | 6 +++ packages/sdk/browser/tsconfig.json | 1 + packages/sdk/browser/vite.config.ts | 18 -------- 5 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 packages/sdk/browser/rollup.config.js delete mode 100644 packages/sdk/browser/vite.config.ts diff --git a/packages/sdk/browser/package.json b/packages/sdk/browser/package.json index 1d1c44388e..60ae7d9f05 100644 --- a/packages/sdk/browser/package.json +++ b/packages/sdk/browser/package.json @@ -27,7 +27,7 @@ ], "scripts": { "clean": "rimraf dist", - "build": "tsc --noEmit && vite build", + "build": "rollup -c rollup.config.js", "lint": "eslint . --ext .ts,.tsx", "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore", "test": "jest", @@ -39,6 +39,10 @@ }, "devDependencies": { "@launchdarkly/private-js-mocks": "0.0.1", + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-terser": "^0.4.3", + "@rollup/plugin-typescript": "^11.1.1", "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/jest": "^29.5.11", "@typescript-eslint/eslint-plugin": "^6.20.0", @@ -54,11 +58,10 @@ "jest-environment-jsdom": "^29.7.0", "prettier": "^3.0.0", "rimraf": "^5.0.5", + "rollup": "^3.23.0", + "rollup-plugin-generate-package-json": "^3.2.0", "ts-jest": "^29.1.1", - "ts-node": "^10.9.2", "typedoc": "0.25.0", - "typescript": "^5.5.3", - "vite": "^5.4.1", - "vite-plugin-dts": "^4.0.3" + "typescript": "^5.5.3" } } diff --git a/packages/sdk/browser/rollup.config.js b/packages/sdk/browser/rollup.config.js new file mode 100644 index 0000000000..d395193e4a --- /dev/null +++ b/packages/sdk/browser/rollup.config.js @@ -0,0 +1,61 @@ +import common from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import terser from '@rollup/plugin-terser'; +import typescript from '@rollup/plugin-typescript'; +import generatePackageJson from 'rollup-plugin-generate-package-json'; + +const getSharedConfig = (format, file) => ({ + input: 'src/index.ts', + output: [ + { + format: format, + sourcemap: true, + file: file, + }, + ], + onwarn: (warning) => { + if (warning.code !== 'CIRCULAR_DEPENDENCY') { + console.error(`(!) ${warning.message}`); + } + }, +}); + +export default [ + { + ...getSharedConfig('es', 'dist/index.es.js'), + plugins: [ + generatePackageJson({ + baseContents: (pkg) => ({ + name: pkg.name, + version: pkg.version, + type: 'module', + }), + }), + typescript({ + module: 'esnext', + }), + common({ + transformMixedEsModules: true, + esmExternals: true, + }), + resolve(), + terser(), + ], + }, + { + ...getSharedConfig('cjs', 'dist/index.cjs.js'), + plugins: [ + generatePackageJson({ + baseContents: (pkg) => ({ + name: pkg.name, + version: pkg.version, + type: 'commonjs', + }), + }), + typescript(), + common(), + resolve(), + terser(), + ], + }, +]; diff --git a/packages/sdk/browser/src/index.ts b/packages/sdk/browser/src/index.ts index a31204dbd8..89e7f67b95 100644 --- a/packages/sdk/browser/src/index.ts +++ b/packages/sdk/browser/src/index.ts @@ -1,3 +1,9 @@ +import DefaultBrowserEventSource from './platform/DefaultBrowserEventSource'; + +// Temporary exports for testing the events source in a browser. +export { DefaultBrowserEventSource }; +export * from '@launchdarkly/js-client-sdk-common'; + export function Hello() { // eslint-disable-next-line no-console console.log('HELLO'); diff --git a/packages/sdk/browser/tsconfig.json b/packages/sdk/browser/tsconfig.json index 79420d3d43..66b350fda3 100644 --- a/packages/sdk/browser/tsconfig.json +++ b/packages/sdk/browser/tsconfig.json @@ -11,6 +11,7 @@ "resolveJsonModule": true, // Uses "." so it can load package.json. "rootDir": ".", + "outDir": "dist", "skipLibCheck": true, // enables importers to jump to source "sourceMap": true, diff --git a/packages/sdk/browser/vite.config.ts b/packages/sdk/browser/vite.config.ts deleted file mode 100644 index 74378ce665..0000000000 --- a/packages/sdk/browser/vite.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -// This file intentionally uses dev dependencies as it is a build file. -import { resolve } from 'path'; -import { defineConfig } from 'vite'; -import dts from 'vite-plugin-dts'; - -export default defineConfig({ - plugins: [dts()], - build: { - lib: { - entry: resolve(__dirname, 'src/index.ts'), - name: '@launchdarkly/js-client-sdk', - fileName: (format) => `index.${format}.js`, - formats: ['cjs', 'es'], - }, - rollupOptions: {}, - }, -}); From fd8d7d63e5e52646e57d521ea022683ed59542ec Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:39:42 -0700 Subject: [PATCH 16/25] Validate package.json imports. --- packages/sdk/browser/package.json | 2 +- packages/sdk/browser/rollup.config.js | 18 +++--------------- packages/sdk/browser/src/index.ts | 5 +++-- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/packages/sdk/browser/package.json b/packages/sdk/browser/package.json index 60ae7d9f05..3f6f646b60 100644 --- a/packages/sdk/browser/package.json +++ b/packages/sdk/browser/package.json @@ -40,6 +40,7 @@ "devDependencies": { "@launchdarkly/private-js-mocks": "0.0.1", "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.0.2", "@rollup/plugin-terser": "^0.4.3", "@rollup/plugin-typescript": "^11.1.1", @@ -59,7 +60,6 @@ "prettier": "^3.0.0", "rimraf": "^5.0.5", "rollup": "^3.23.0", - "rollup-plugin-generate-package-json": "^3.2.0", "ts-jest": "^29.1.1", "typedoc": "0.25.0", "typescript": "^5.5.3" diff --git a/packages/sdk/browser/rollup.config.js b/packages/sdk/browser/rollup.config.js index d395193e4a..d78a55a79d 100644 --- a/packages/sdk/browser/rollup.config.js +++ b/packages/sdk/browser/rollup.config.js @@ -2,7 +2,7 @@ import common from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; import terser from '@rollup/plugin-terser'; import typescript from '@rollup/plugin-typescript'; -import generatePackageJson from 'rollup-plugin-generate-package-json'; +import json from '@rollup/plugin-json'; const getSharedConfig = (format, file) => ({ input: 'src/index.ts', @@ -24,13 +24,6 @@ export default [ { ...getSharedConfig('es', 'dist/index.es.js'), plugins: [ - generatePackageJson({ - baseContents: (pkg) => ({ - name: pkg.name, - version: pkg.version, - type: 'module', - }), - }), typescript({ module: 'esnext', }), @@ -40,22 +33,17 @@ export default [ }), resolve(), terser(), + json(), ], }, { ...getSharedConfig('cjs', 'dist/index.cjs.js'), plugins: [ - generatePackageJson({ - baseContents: (pkg) => ({ - name: pkg.name, - version: pkg.version, - type: 'commonjs', - }), - }), typescript(), common(), resolve(), terser(), + json(), ], }, ]; diff --git a/packages/sdk/browser/src/index.ts b/packages/sdk/browser/src/index.ts index 89e7f67b95..e736320369 100644 --- a/packages/sdk/browser/src/index.ts +++ b/packages/sdk/browser/src/index.ts @@ -1,7 +1,8 @@ +import BrowserInfo from './platform/BrowserInfo'; import DefaultBrowserEventSource from './platform/DefaultBrowserEventSource'; -// Temporary exports for testing the events source in a browser. -export { DefaultBrowserEventSource }; +// Temporary exports for testing in a browser. +export { DefaultBrowserEventSource, BrowserInfo }; export * from '@launchdarkly/js-client-sdk-common'; export function Hello() { From 61a36316bb3961dcc6a5ba45c1b00fb95ca5a3a9 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:08:18 -0700 Subject: [PATCH 17/25] Configurable reset time. --- .../__tests__/platform/Backoff.test.ts | 68 ++++++++++++------- packages/sdk/browser/src/platform/Backoff.ts | 7 +- .../src/platform/DefaultBrowserEventSource.ts | 2 +- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/packages/sdk/browser/__tests__/platform/Backoff.test.ts b/packages/sdk/browser/__tests__/platform/Backoff.test.ts index 3d32614ae8..49cdd901cb 100644 --- a/packages/sdk/browser/__tests__/platform/Backoff.test.ts +++ b/packages/sdk/browser/__tests__/platform/Backoff.test.ts @@ -2,27 +2,28 @@ import Backoff from '../../src/platform/Backoff'; const noJitter = (): number => 0; const maxJitter = (): number => 1; +const defaultResetInterval = 60 * 1000; it.each([1, 1000, 5000])('has the correct starting delay', (initialDelay) => { - const backoff = new Backoff(initialDelay, noJitter); + const backoff = new Backoff(initialDelay, defaultResetInterval, noJitter); expect(backoff.fail()).toEqual(initialDelay); }); it.each([1, 1000, 5000])('doubles delay on consecutive failures', (initialDelay) => { - const backoff = new Backoff(initialDelay, noJitter); + const backoff = new Backoff(initialDelay, defaultResetInterval, noJitter); expect(backoff.fail()).toEqual(initialDelay); expect(backoff.fail()).toEqual(initialDelay * 2); expect(backoff.fail()).toEqual(initialDelay * 4); }); it('stops increasing delay when the max backoff is encountered', () => { - const backoff = new Backoff(5000, noJitter); + const backoff = new Backoff(5000, defaultResetInterval, noJitter); expect(backoff.fail()).toEqual(5000); expect(backoff.fail()).toEqual(10000); expect(backoff.fail()).toEqual(20000); expect(backoff.fail()).toEqual(30000); - const backoff2 = new Backoff(1000, noJitter); + const backoff2 = new Backoff(1000, defaultResetInterval, noJitter); expect(backoff2.fail()).toEqual(1000); expect(backoff2.fail()).toEqual(2000); expect(backoff2.fail()).toEqual(4000); @@ -32,12 +33,12 @@ it('stops increasing delay when the max backoff is encountered', () => { }); it('handles an initial retry delay longer than the maximum retry delay', () => { - const backoff = new Backoff(40000, noJitter); + const backoff = new Backoff(40000, defaultResetInterval, noJitter); expect(backoff.fail()).toEqual(30000); }); it('jitters the backoff value', () => { - const backoff = new Backoff(1000, maxJitter); + const backoff = new Backoff(1000, defaultResetInterval, maxJitter); expect(backoff.fail()).toEqual(500); expect(backoff.fail()).toEqual(1000); expect(backoff.fail()).toEqual(2000); @@ -46,22 +47,41 @@ it('jitters the backoff value', () => { expect(backoff.fail()).toEqual(15000); }); -it('resets the delay when the last successful connection was connected greater than RESET_INTERVAL', () => { - const backoff = new Backoff(1000, noJitter); - expect(backoff.fail(1000)).toEqual(1000); - backoff.success(2000); - expect(backoff.fail(62001)).toEqual(1000); - expect(backoff.fail(62002)).toEqual(2000); - backoff.success(64002); - expect(backoff.fail(124003)).toEqual(1000); -}); +it.each([10 * 1000, 60 * 1000])( + 'resets the delay when the last successful connection was connected greater than the retry reset interval', + (retryResetInterval) => { + let time = 1000; + const backoff = new Backoff(1000, retryResetInterval, noJitter); + expect(backoff.fail(time)).toEqual(1000); + time += 1; + backoff.success(time); + time = time + retryResetInterval + 1; + expect(backoff.fail(time)).toEqual(1000); + time += 1; + expect(backoff.fail(time)).toEqual(2000); + time += 1; + backoff.success(time); + time = time + retryResetInterval + 1; + expect(backoff.fail(time)).toEqual(1000); + }, +); -it('does not reset the delay when the connection did not persist longer than the RESET_INTERVAL', () => { - const backoff = new Backoff(1000, noJitter); - expect(backoff.fail(1000)).toEqual(1000); - backoff.success(2000); - expect(backoff.fail(61000)).toEqual(2000); - expect(backoff.fail(120000)).toEqual(4000); - backoff.success(124000); - expect(backoff.fail(183000)).toEqual(8000); -}); +it.each([10 * 1000, 60 * 1000])( + 'does not reset the delay when the connection did not persist longer than the retry reset interval', + (retryResetInterval) => { + const backoff = new Backoff(1000, retryResetInterval, noJitter); + + let time = 1000; + expect(backoff.fail(time)).toEqual(1000); + time += 1; + backoff.success(time); + time += retryResetInterval; + expect(backoff.fail(time)).toEqual(2000); + time += retryResetInterval; + expect(backoff.fail(time)).toEqual(4000); + time += 1; + backoff.success(time); + time += retryResetInterval; + expect(backoff.fail(time)).toEqual(8000); + }, +); diff --git a/packages/sdk/browser/src/platform/Backoff.ts b/packages/sdk/browser/src/platform/Backoff.ts index 59e28b5b82..f90bcd7c4d 100644 --- a/packages/sdk/browser/src/platform/Backoff.ts +++ b/packages/sdk/browser/src/platform/Backoff.ts @@ -1,6 +1,5 @@ const MAX_RETRY_DELAY = 30 * 1000; // Maximum retry delay 30 seconds. const JITTER_RATIO = 0.5; // Delay should be 50%-100% of calculated time. -const RESET_INTERVAL = 60 * 1000; // Reset interval in seconds. /** * Implements exponential backoff and jitter. This class tracks successful connections and failures @@ -24,6 +23,7 @@ export default class Backoff { constructor( initialRetryDelayMillis: number, + private readonly retryResetIntervalMillis: number, private readonly random = Math.random, ) { // Initial retry delay cannot be 0. @@ -62,7 +62,10 @@ export default class Backoff { fail(timeStampMs: number = Date.now()): number { // If the last successful connection was active for more than the RESET_INTERVAL, then we // return to the initial retry delay. - if (this.activeSince !== undefined && timeStampMs - this.activeSince > RESET_INTERVAL) { + if ( + this.activeSince !== undefined && + timeStampMs - this.activeSince > this.retryResetIntervalMillis + ) { this.retryCount = 0; } this.activeSince = undefined; diff --git a/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts b/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts index c560c86daf..b9084349bc 100644 --- a/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts +++ b/packages/sdk/browser/src/platform/DefaultBrowserEventSource.ts @@ -34,7 +34,7 @@ export default class DefaultBrowserEventSource implements LDEventSource { private readonly url: string, options: EventSourceInitDict, ) { - this.backoff = new Backoff(options.initialRetryDelayMillis); + this.backoff = new Backoff(options.initialRetryDelayMillis, options.retryResetIntervalMillis); this.errorFilter = options.errorFilter; this.openConnection(); } From 0cb22bfec5efe786d291c965c0120f628bf32e28 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:25:52 -0700 Subject: [PATCH 18/25] feat: Scaffold browser client. --- packages/sdk/browser/src/BrowserClient.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 packages/sdk/browser/src/BrowserClient.ts diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts new file mode 100644 index 0000000000..48fb94e184 --- /dev/null +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -0,0 +1,15 @@ +import { AutoEnvAttributes, LDClient as CommonClient, ConnectionMode, LDClientImpl, LDContext, LDEvaluationDetail, LDEvaluationDetailTyped, LDFlagSet, LDFlagValue, LDLogger, LDOptions } from '@launchdarkly/js-client-sdk-common'; +import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common/dist/api/LDIdentifyOptions'; +import BrowserPlatform from './platform/BrowserPlatform'; + +/** + * We are not supporting dynamically setting the connection mode on the LDClient. + */ +export type LDClient = Omit; + + +export class BrowserClient extends LDClientImpl { + constructor(clientSideId: string, autoEnvAttributes: AutoEnvAttributes, options: LDOptions = {}){ + super(clientSideId, autoEnvAttributes, new BrowserPlatform(options)) + } +} From af341cb7a42e71d8253c822d920ade1aab26682a Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:26:32 -0700 Subject: [PATCH 19/25] Implements platform. --- packages/sdk/browser/src/platform/BrowserPlatform.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/browser/src/platform/BrowserPlatform.ts b/packages/sdk/browser/src/platform/BrowserPlatform.ts index 3267969c40..34dfccaefa 100644 --- a/packages/sdk/browser/src/platform/BrowserPlatform.ts +++ b/packages/sdk/browser/src/platform/BrowserPlatform.ts @@ -2,8 +2,8 @@ import { Crypto, Encoding, Info, - /* platform */ LDOptions, + Platform, Requests, Storage, } from '@launchdarkly/js-client-sdk-common'; @@ -14,7 +14,7 @@ import BrowserInfo from './BrowserInfo'; import BrowserRequests from './BrowserRequests'; import LocalStorage, { isLocalStorageSupported } from './LocalStorage'; -export default class BrowserPlatform /* implements platform.Platform */ { +export default class BrowserPlatform implements Platform { encoding: Encoding = new BrowserEncoding(); info: Info = new BrowserInfo(); // fileSystem?: Filesystem; From 3900a132955d9b7e859ec2ec5ad570825dd7abec Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:51:09 -0700 Subject: [PATCH 20/25] feat: Scaffold browser client. --- packages/sdk/browser/src/BrowserClient.ts | 37 +++++++++++++++++--- packages/sdk/browser/src/index.ts | 42 ++++++++++++++++++----- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index 48fb94e184..bd46e7ec00 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -1,5 +1,12 @@ -import { AutoEnvAttributes, LDClient as CommonClient, ConnectionMode, LDClientImpl, LDContext, LDEvaluationDetail, LDEvaluationDetailTyped, LDFlagSet, LDFlagValue, LDLogger, LDOptions } from '@launchdarkly/js-client-sdk-common'; -import { LDIdentifyOptions } from '@launchdarkly/js-client-sdk-common/dist/api/LDIdentifyOptions'; +import { + AutoEnvAttributes, + base64UrlEncode, + LDClient as CommonClient, + LDClientImpl, + LDContext, + LDOptions, +} from '@launchdarkly/js-client-sdk-common'; + import BrowserPlatform from './platform/BrowserPlatform'; /** @@ -7,9 +14,29 @@ import BrowserPlatform from './platform/BrowserPlatform'; */ export type LDClient = Omit; - export class BrowserClient extends LDClientImpl { - constructor(clientSideId: string, autoEnvAttributes: AutoEnvAttributes, options: LDOptions = {}){ - super(clientSideId, autoEnvAttributes, new BrowserPlatform(options)) + constructor( + private readonly clientSideId: string, + autoEnvAttributes: AutoEnvAttributes, + options: LDOptions = {}, + ) { + super(clientSideId, autoEnvAttributes, new BrowserPlatform(options), options, { + analyticsEventPath: `/events/bulk/${clientSideId}`, + diagnosticEventPath: `/events/diagnostic/${clientSideId}`, + includeAuthorizationHeader: false, + highTimeoutThreshold: 5, + }); + } + + private encodeContext(context: LDContext) { + return base64UrlEncode(JSON.stringify(context), this.platform.encoding!); + } + + override createStreamUriPath(context: LDContext) { + return `/eval/${this.clientSideId}/${this.encodeContext(context)}`; + } + + override createPollUriPath(context: LDContext): string { + return `/sdk/evalx/${this.clientSideId}/contexts/${this.encodeContext(context)}`; } } diff --git a/packages/sdk/browser/src/index.ts b/packages/sdk/browser/src/index.ts index e736320369..5e25241ec9 100644 --- a/packages/sdk/browser/src/index.ts +++ b/packages/sdk/browser/src/index.ts @@ -1,11 +1,37 @@ -import BrowserInfo from './platform/BrowserInfo'; -import DefaultBrowserEventSource from './platform/DefaultBrowserEventSource'; +import { + AutoEnvAttributes, + LDContext, + LDContextCommon, + LDContextMeta, + LDFlagSet, + LDLogger, + LDLogLevel, + LDMultiKindContext, + LDOptions, + LDSingleKindContext, +} from '@launchdarkly/js-client-sdk-common'; -// Temporary exports for testing in a browser. -export { DefaultBrowserEventSource, BrowserInfo }; -export * from '@launchdarkly/js-client-sdk-common'; +import { BrowserClient, LDClient } from './BrowserClient'; -export function Hello() { - // eslint-disable-next-line no-console - console.log('HELLO'); +// TODO: Export and use browser specific options. +export { + LDClient, + AutoEnvAttributes, + LDOptions, + LDFlagSet, + LDContext, + LDContextCommon, + LDContextMeta, + LDMultiKindContext, + LDSingleKindContext, + LDLogLevel, + LDLogger, +}; + +export function init( + clientSideId: string, + autoEnvAttributes: AutoEnvAttributes, + options?: LDOptions, +): LDClient { + return new BrowserClient(clientSideId, autoEnvAttributes, options); } From ff1dc23d4bead265a92077e99424a04a269b3b57 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:58:35 -0700 Subject: [PATCH 21/25] Correct fetch. --- packages/sdk/browser/src/platform/BrowserRequests.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sdk/browser/src/platform/BrowserRequests.ts b/packages/sdk/browser/src/platform/BrowserRequests.ts index 52d4d0dbd4..5e73467843 100644 --- a/packages/sdk/browser/src/platform/BrowserRequests.ts +++ b/packages/sdk/browser/src/platform/BrowserRequests.ts @@ -11,7 +11,8 @@ import DefaultBrowserEventSource from './DefaultBrowserEventSource'; export default class BrowserRequests implements Requests { fetch(url: string, options?: Options): Promise { - return this.fetch(url, options); + // @ts-ignore + return fetch(url, options); } createEventSource(url: string, eventSourceInitDict: EventSourceInitDict): LDEventSource { From a1ad827d4d975ec2c8ccb6cbb3529a3dd4da1f62 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:12:43 -0700 Subject: [PATCH 22/25] feat: Allow using custom user-agent name. --- .../src/internal/events/LDInternalOptions.ts | 1 + .../stream/StreamingProcessor.test.ts | 12 ++++- .../src/internal/stream/StreamingProcessor.ts | 10 ++-- packages/shared/common/src/utils/http.ts | 6 ++- .../shared/mocks/src/streamingProcessor.ts | 3 +- .../sdk-client/__tests__/LDClientImpl.test.ts | 4 +- .../polling/PollingProcessor.test.ts | 52 ++++++------------- .../shared/sdk-client/src/LDClientImpl.ts | 16 ++++-- .../src/configuration/Configuration.ts | 3 ++ .../src/polling/PollingProcessor.ts | 7 +-- .../sdk-client/src/polling/Requestor.ts | 7 ++- .../__tests__/data_sources/Requestor.test.ts | 10 ++-- .../shared/sdk-server/src/LDClientImpl.ts | 6 ++- .../sdk-server/src/data_sources/Requestor.ts | 6 +-- 14 files changed, 72 insertions(+), 71 deletions(-) diff --git a/packages/shared/common/src/internal/events/LDInternalOptions.ts b/packages/shared/common/src/internal/events/LDInternalOptions.ts index b852990a3e..54f6b91c54 100644 --- a/packages/shared/common/src/internal/events/LDInternalOptions.ts +++ b/packages/shared/common/src/internal/events/LDInternalOptions.ts @@ -12,6 +12,7 @@ export type LDInternalOptions = { analyticsEventPath?: string; diagnosticEventPath?: string; includeAuthorizationHeader?: boolean; + userAgentHeaderName?: 'user-agent' | 'x-launchdarkly-user-agent'; /** * In seconds. Log a warning if identifyTimeout is greater than this value. diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts index e91b93e169..7defe4afab 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts @@ -101,7 +101,6 @@ describe('given a stream processor with mock event source', () => { diagnosticsManager = new DiagnosticsManager(sdkKey, basicPlatform, {}); streamingProcessor = new StreamingProcessor( - sdkKey, { basicConfiguration: getBasicConfiguration(logger), platform: basicPlatform, @@ -109,6 +108,11 @@ describe('given a stream processor with mock event source', () => { '/all', [], listeners, + { + authorization: 'my-sdk-key', + 'user-agent': 'TestUserAgent/2.0.2', + 'x-launchdarkly-wrapper': 'Rapper/1.2.3', + }, diagnosticsManager, mockErrorHandler, ); @@ -137,7 +141,6 @@ describe('given a stream processor with mock event source', () => { it('sets streamInitialReconnectDelay correctly', () => { streamingProcessor = new StreamingProcessor( - sdkKey, { basicConfiguration: getBasicConfiguration(logger), platform: basicPlatform, @@ -145,6 +148,11 @@ describe('given a stream processor with mock event source', () => { '/all', [], listeners, + { + authorization: 'my-sdk-key', + 'user-agent': 'TestUserAgent/2.0.2', + 'x-launchdarkly-wrapper': 'Rapper/1.2.3', + }, diagnosticsManager, mockErrorHandler, 22, diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.ts index d9ccfaab45..a4d115e40b 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.ts @@ -10,7 +10,7 @@ import { LDStreamProcessor } from '../../api/subsystem'; import { LDStreamingError } from '../../errors'; import { ClientContext } from '../../options'; import { getStreamingUri } from '../../options/ServiceEndpoints'; -import { defaultHeaders, httpErrorMessage, shouldRetry } from '../../utils'; +import { httpErrorMessage, LDHeaders, shouldRetry } from '../../utils'; import { DiagnosticsManager } from '../diagnostics'; import { StreamingErrorHandler } from './types'; @@ -35,20 +35,20 @@ class StreamingProcessor implements LDStreamProcessor { private connectionAttemptStartTime?: number; constructor( - sdkKey: string, clientContext: ClientContext, streamUriPath: string, parameters: { key: string; value: string }[], private readonly listeners: Map, + baseHeaders: LDHeaders, private readonly diagnosticsManager?: DiagnosticsManager, private readonly errorHandler?: StreamingErrorHandler, private readonly streamInitialReconnectDelay = 1, ) { const { basicConfiguration, platform } = clientContext; - const { logger, tags } = basicConfiguration; - const { info, requests } = platform; + const { logger } = basicConfiguration; + const { requests } = platform; - this.headers = defaultHeaders(sdkKey, info, tags); + this.headers = { ...baseHeaders }; this.logger = logger; this.requests = requests; this.streamUri = getStreamingUri( diff --git a/packages/shared/common/src/utils/http.ts b/packages/shared/common/src/utils/http.ts index 0a9885780a..347c63f4eb 100644 --- a/packages/shared/common/src/utils/http.ts +++ b/packages/shared/common/src/utils/http.ts @@ -4,7 +4,8 @@ import { ApplicationTags } from '../options'; export type LDHeaders = { authorization?: string; - 'user-agent': string; + 'user-agent'?: string; + 'x-launchdarkly-user-agent'?: string; 'x-launchdarkly-wrapper'?: string; 'x-launchdarkly-tags'?: string; }; @@ -14,11 +15,12 @@ export function defaultHeaders( info: Info, tags?: ApplicationTags, includeAuthorizationHeader: boolean = true, + userAgentHeaderName: 'user-agent' | 'x-launchdarkly-user-agent' = 'user-agent', ): LDHeaders { const { userAgentBase, version, wrapperName, wrapperVersion } = info.sdkData(); const headers: LDHeaders = { - 'user-agent': `${userAgentBase ?? 'NodeJSClient'}/${version}`, + [userAgentHeaderName]: `${userAgentBase ?? 'NodeJSClient'}/${version}`, }; // edge sdks sets this to false because they use the clientSideID diff --git a/packages/shared/mocks/src/streamingProcessor.ts b/packages/shared/mocks/src/streamingProcessor.ts index e596b443f1..cd038e7ef2 100644 --- a/packages/shared/mocks/src/streamingProcessor.ts +++ b/packages/shared/mocks/src/streamingProcessor.ts @@ -2,6 +2,7 @@ import type { ClientContext, EventName, internal, + LDHeaders, LDStreamingError, ProcessStreamResponse, } from '@common'; @@ -22,11 +23,11 @@ export const setupMockStreamingProcessor = ( MockStreamingProcessor.mockImplementation( ( - sdkKey: string, clientContext: ClientContext, streamUriPath: string, parameters: { key: string; value: string }[], listeners: Map, + baseHeaders: LDHeaders, diagnosticsManager: internal.DiagnosticsManager, errorHandler: internal.StreamingErrorHandler, _streamInitialReconnectDelay: number, diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts index b0d404cab0..648be27661 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts @@ -108,11 +108,11 @@ describe('sdk-client object', () => { 'dev-test-flag': false, }); expect(MockStreamingProcessor).toHaveBeenCalledWith( - expect.anything(), expect.anything(), '/stream/path', expect.anything(), expect.anything(), + expect.anything(), undefined, expect.anything(), ); @@ -129,11 +129,11 @@ describe('sdk-client object', () => { await ldc.identify(carContext); expect(MockStreamingProcessor).toHaveBeenCalledWith( - expect.anything(), expect.anything(), '/stream/path', [{ key: 'withReasons', value: 'true' }], expect.anything(), + expect.anything(), undefined, expect.anything(), ); diff --git a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts index 8e6a39c931..71ca351ed1 100644 --- a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts @@ -4,11 +4,8 @@ import { EventSource, EventSourceCapabilities, EventSourceInitDict, - Info, - PlatformData, Requests, Response, - SdkData, } from '@launchdarkly/js-sdk-common'; import PollingProcessor, { PollingConfig } from '../../src/polling/PollingProcessor'; @@ -56,13 +53,6 @@ function makeRequests(): Requests { }; } -function makeInfo(sdkData: SdkData = {}, platformData: PlatformData = {}): Info { - return { - sdkData: () => sdkData, - platformData: () => platformData, - }; -} - function makeConfig(config?: { pollInterval?: number; useReport?: boolean }): PollingConfig { return { pollInterval: config?.pollInterval ?? 60 * 5, @@ -90,12 +80,11 @@ it('makes no requests until it is started', () => { const requests = makeRequests(); // eslint-disable-next-line no-new new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig(), + {}, (_flags) => {}, (_error) => {}, ); @@ -107,12 +96,11 @@ it('polls immediately when started', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig(), + {}, (_flags) => {}, (_error) => {}, ); @@ -128,12 +116,11 @@ it('calls callback on success', async () => { const errorCallback = jest.fn(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig(), + {}, dataCallback, errorCallback, ); @@ -150,12 +137,11 @@ it('polls repeatedly', async () => { requests.fetch = mockFetch('{ "flagA": true }', 200); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig({ pollInterval: 0.1 }), + {}, dataCallback, errorCallback, ); @@ -189,12 +175,11 @@ it('stops polling when stopped', (done) => { const errorCallback = jest.fn(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/stops', [], makeConfig({ pollInterval: 0.01 }), + {}, dataCallback, errorCallback, ); @@ -212,15 +197,14 @@ it('includes the correct headers on requests', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo({ - userAgentBase: 'AnSDK', - version: '42', - }), '/polling', [], makeConfig(), + { + authorization: 'the-sdk-key', + 'user-agent': 'AnSDK/42', + }, (_flags) => {}, (_error) => {}, ); @@ -242,12 +226,11 @@ it('defaults to using the "GET" verb', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig(), + {}, (_flags) => {}, (_error) => {}, ); @@ -266,12 +249,11 @@ it('can be configured to use the "REPORT" verb', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig({ useReport: true }), + {}, (_flags) => {}, (_error) => {}, ); @@ -293,12 +275,11 @@ it('continues polling after receiving bad JSON', async () => { const config = makeConfig({ pollInterval: 0.1 }); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], config, + {}, dataCallback, errorCallback, ); @@ -322,12 +303,11 @@ it('continues polling after an exception thrown during a request', async () => { const config = makeConfig({ pollInterval: 0.1 }); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], config, + {}, dataCallback, errorCallback, ); @@ -354,12 +334,11 @@ it('can handle recoverable http errors', async () => { const config = makeConfig({ pollInterval: 0.1 }); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], config, + {}, dataCallback, errorCallback, ); @@ -384,12 +363,11 @@ it('stops polling on unrecoverable error codes', (done) => { const config = makeConfig({ pollInterval: 0.01 }); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], config, + {}, dataCallback, errorCallback, ); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index beac5c1c03..d1c0ac331a 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -3,11 +3,13 @@ import { ClientContext, clone, Context, + defaultHeaders, internal, LDClientError, LDContext, LDFlagSet, LDFlagValue, + LDHeaders, LDLogger, Platform, ProcessStreamResponse, @@ -60,6 +62,7 @@ export default class LDClientImpl implements LDClient { private eventSendingEnabled: boolean = true; private networkAvailable: boolean = true; private connectionMode: ConnectionMode; + private baseHeaders: LDHeaders; /** * Creates the client object synchronously. No async, no network calls. @@ -109,6 +112,14 @@ export default class LDClientImpl implements LDClient { const ldContext = Context.toLDContext(context); this.emitter.emit('change', ldContext, flagKeys); }); + + this.baseHeaders = defaultHeaders( + this.sdkKey, + this.platform.info, + this.config.tags, + true, + 'x-launchdarkly-user-agent', + ); } /** @@ -407,12 +418,11 @@ export default class LDClientImpl implements LDClient { } this.updateProcessor = new PollingProcessor( - this.sdkKey, this.clientContext.platform.requests, - this.clientContext.platform.info, this.createPollUriPath(context), parameters, this.config, + this.baseHeaders, async (flags) => { this.logger.debug(`Handling polling result: ${Object.keys(flags)}`); @@ -446,11 +456,11 @@ export default class LDClientImpl implements LDClient { } this.updateProcessor = new internal.StreamingProcessor( - this.sdkKey, this.clientContext, this.createStreamUriPath(context), parameters, this.createStreamListeners(checkedContext, identifyResolve), + this.baseHeaders, this.diagnosticsManager, (e) => { identifyReject(e); diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index 55d87f879e..cef4678c93 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -64,6 +64,8 @@ export default class Configuration { public readonly pollInterval: number = DEFAULT_POLLING_INTERVAL; + public readonly userAgentHeaderName: 'user-agent' | 'x-launchdarkly-user-agent'; + // Allow indexing Configuration by a string [index: string]: any; @@ -81,6 +83,7 @@ export default class Configuration { pristineOptions.payloadFilterKey, ); this.tags = new ApplicationTags({ application: this.applicationInfo, logger: this.logger }); + this.userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent'; } validateTypesAndNames(pristineOptions: LDOptions): string[] { diff --git a/packages/shared/sdk-client/src/polling/PollingProcessor.ts b/packages/shared/sdk-client/src/polling/PollingProcessor.ts index 2b95f27fd2..05086685ca 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessor.ts +++ b/packages/shared/sdk-client/src/polling/PollingProcessor.ts @@ -1,10 +1,12 @@ import { ApplicationTags, + defaultHeaders, getPollingUri, httpErrorMessage, HttpErrorResponse, Info, isHttpRecoverable, + LDHeaders, LDLogger, LDPollingError, Requests, @@ -45,12 +47,11 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { private requestor: Requestor; constructor( - sdkKey: string, requests: Requests, - info: Info, uriPath: string, parameters: { key: string; value: string }[], config: PollingConfig, + baseHeaders: LDHeaders, private readonly dataHandler: (flags: Flags) => void, private readonly errorHandler?: PollingErrorHandler, ) { @@ -58,7 +59,7 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { this.logger = config.logger; this.pollInterval = config.pollInterval; - this.requestor = new Requestor(sdkKey, requests, info, uri, config.useReport, config.tags); + this.requestor = new Requestor(requests, uri, config.useReport, baseHeaders); } private async poll() { diff --git a/packages/shared/sdk-client/src/polling/Requestor.ts b/packages/shared/sdk-client/src/polling/Requestor.ts index 6a46dfcff2..bdbe449c46 100644 --- a/packages/shared/sdk-client/src/polling/Requestor.ts +++ b/packages/shared/sdk-client/src/polling/Requestor.ts @@ -4,6 +4,7 @@ import { defaultHeaders, HttpErrorResponse, Info, + LDHeaders, Requests, } from '@launchdarkly/js-sdk-common'; @@ -32,14 +33,12 @@ export default class Requestor { private verb: string; constructor( - sdkKey: string, private requests: Requests, - info: Info, private readonly uri: string, useReport: boolean, - tags: ApplicationTags, + baseHeaders: LDHeaders, ) { - this.headers = defaultHeaders(sdkKey, info, tags); + this.headers = { ...baseHeaders }; this.verb = useReport ? 'REPORT' : 'GET'; } 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 46a0640ac8..3f3d8537a2 100644 --- a/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts +++ b/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts @@ -6,7 +6,6 @@ import { Requests, Response, } from '@launchdarkly/js-sdk-common'; -import { createBasicPlatform } from '@launchdarkly/private-js-mocks'; import promisify from '../../src/async/promisify'; import Requestor from '../../src/data_sources/Requestor'; @@ -80,12 +79,9 @@ describe('given a requestor', () => { }, }; - requestor = new Requestor( - 'sdkKey', - new Configuration({}), - createBasicPlatform().info, - requests, - ); + requestor = new Requestor(new Configuration({}), requests, { + authorization: 'sdkKey', + }); }); it('gets data', (done) => { diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index ef6c85840f..5f2b177640 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -3,6 +3,7 @@ import { cancelableTimedPromise, ClientContext, Context, + defaultHeaders, internal, LDClientError, LDContext, @@ -207,6 +208,7 @@ export default class LDClientImpl implements LDClient { }, }; this.evaluator = new Evaluator(this.platform, queries); + const baseHeaders = defaultHeaders(sdkKey, platform.info, config.tags); const listeners = createStreamListeners(dataSourceUpdates, this.logger, { put: () => this.initSuccess(), @@ -214,18 +216,18 @@ export default class LDClientImpl implements LDClient { const makeDefaultProcessor = () => config.stream ? new internal.StreamingProcessor( - sdkKey, clientContext, '/all', [], listeners, + baseHeaders, this.diagnosticsManager, (e) => this.dataSourceErrorHandler(e), this.config.streamInitialReconnectDelay, ) : new PollingProcessor( config, - new Requestor(sdkKey, config, this.platform.info, this.platform.requests), + new Requestor(config, this.platform.requests, baseHeaders), dataSourceUpdates, () => this.initSuccess(), (e) => this.dataSourceErrorHandler(e), diff --git a/packages/shared/sdk-server/src/data_sources/Requestor.ts b/packages/shared/sdk-server/src/data_sources/Requestor.ts index d6498c6047..235e46f737 100644 --- a/packages/shared/sdk-server/src/data_sources/Requestor.ts +++ b/packages/shared/sdk-server/src/data_sources/Requestor.ts @@ -2,6 +2,7 @@ import { defaultHeaders, getPollingUri, Info, + LDHeaders, LDStreamingError, Options, Requests, @@ -28,12 +29,11 @@ export default class Requestor implements LDFeatureRequestor { > = {}; constructor( - sdkKey: string, config: Configuration, - info: Info, private readonly requests: Requests, + baseHeaders: LDHeaders, ) { - this.headers = defaultHeaders(sdkKey, info, config.tags); + this.headers = { ...baseHeaders }; this.uri = getPollingUri(config.serviceEndpoints, '/sdk/latest-all', []); } From 745e994ba0e5f816b12acb4df0d0fc83701db1cf Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:44:10 -0700 Subject: [PATCH 23/25] Remove unused imports. --- .../shared/sdk-client/src/polling/PollingProcessor.ts | 2 -- packages/shared/sdk-client/src/polling/Requestor.ts | 9 +-------- packages/shared/sdk-server/src/data_sources/Requestor.ts | 2 -- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/shared/sdk-client/src/polling/PollingProcessor.ts b/packages/shared/sdk-client/src/polling/PollingProcessor.ts index 05086685ca..5a11add60d 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessor.ts +++ b/packages/shared/sdk-client/src/polling/PollingProcessor.ts @@ -1,10 +1,8 @@ import { ApplicationTags, - defaultHeaders, getPollingUri, httpErrorMessage, HttpErrorResponse, - Info, isHttpRecoverable, LDHeaders, LDLogger, diff --git a/packages/shared/sdk-client/src/polling/Requestor.ts b/packages/shared/sdk-client/src/polling/Requestor.ts index bdbe449c46..ec433b5508 100644 --- a/packages/shared/sdk-client/src/polling/Requestor.ts +++ b/packages/shared/sdk-client/src/polling/Requestor.ts @@ -1,12 +1,5 @@ // eslint-disable-next-line max-classes-per-file -import { - ApplicationTags, - defaultHeaders, - HttpErrorResponse, - Info, - LDHeaders, - Requests, -} from '@launchdarkly/js-sdk-common'; +import { HttpErrorResponse, LDHeaders, Requests } from '@launchdarkly/js-sdk-common'; function isOk(status: number) { return status >= 200 && status <= 299; diff --git a/packages/shared/sdk-server/src/data_sources/Requestor.ts b/packages/shared/sdk-server/src/data_sources/Requestor.ts index 235e46f737..de56a0729b 100644 --- a/packages/shared/sdk-server/src/data_sources/Requestor.ts +++ b/packages/shared/sdk-server/src/data_sources/Requestor.ts @@ -1,7 +1,5 @@ import { - defaultHeaders, getPollingUri, - Info, LDHeaders, LDStreamingError, Options, From 5126f938a2b67252bae0bd5bdbd7083c59125339 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:12:54 -0700 Subject: [PATCH 24/25] Event processor should use the default headers. --- .../internal/events/EventProcessor.test.ts | 8 +++++++- .../src/internal/events/EventProcessor.ts | 4 +++- .../src/internal/events/EventSender.test.ts | 6 ++++++ .../common/src/internal/events/EventSender.ts | 12 +++++------- packages/shared/sdk-client/src/LDClientImpl.ts | 18 ++++++++++-------- .../src/events/createEventProcessor.ts | 4 +++- .../__tests__/events/EventProcessor.test.ts | 1 + packages/shared/sdk-server/src/LDClientImpl.ts | 3 ++- 8 files changed, 37 insertions(+), 19 deletions(-) diff --git a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts index d32f78c66c..e972fa46e0 100644 --- a/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts +++ b/packages/shared/common/__tests__/internal/events/EventProcessor.test.ts @@ -131,7 +131,12 @@ describe('given an event processor', () => { }), ); contextDeduplicator = new ContextDeduplicator(); - eventProcessor = new EventProcessor(eventProcessorConfig, clientContext, contextDeduplicator); + eventProcessor = new EventProcessor( + eventProcessorConfig, + clientContext, + {}, + contextDeduplicator, + ); }); afterEach(() => { @@ -788,6 +793,7 @@ describe('given an event processor', () => { eventProcessor = new EventProcessor( eventProcessorConfig, clientContextWithDebug, + {}, contextDeduplicator, ); diff --git a/packages/shared/common/src/internal/events/EventProcessor.ts b/packages/shared/common/src/internal/events/EventProcessor.ts index bda2fe7b8e..8743494214 100644 --- a/packages/shared/common/src/internal/events/EventProcessor.ts +++ b/packages/shared/common/src/internal/events/EventProcessor.ts @@ -5,6 +5,7 @@ import LDEventProcessor from '../../api/subsystem/LDEventProcessor'; import AttributeReference from '../../AttributeReference'; import ContextFilter from '../../ContextFilter'; import { ClientContext } from '../../options'; +import { LDHeaders } from '../../utils'; import { DiagnosticsManager } from '../diagnostics'; import EventSender from './EventSender'; import EventSummarizer, { SummarizedFlagsEvent } from './EventSummarizer'; @@ -106,13 +107,14 @@ export default class EventProcessor implements LDEventProcessor { constructor( private readonly config: EventProcessorOptions, clientContext: ClientContext, + baseHeaders: LDHeaders, private readonly contextDeduplicator?: LDContextDeduplicator, private readonly diagnosticsManager?: DiagnosticsManager, start: boolean = true, ) { this.capacity = config.eventsCapacity; this.logger = clientContext.basicConfiguration.logger; - this.eventSender = new EventSender(clientContext); + this.eventSender = new EventSender(clientContext, baseHeaders); this.contextFilter = new ContextFilter( config.allAttributesPrivate, diff --git a/packages/shared/common/src/internal/events/EventSender.test.ts b/packages/shared/common/src/internal/events/EventSender.test.ts index 5e0e348e29..3bd27d77cf 100644 --- a/packages/shared/common/src/internal/events/EventSender.test.ts +++ b/packages/shared/common/src/internal/events/EventSender.test.ts @@ -112,6 +112,12 @@ describe('given an event sender', () => { eventSender = new EventSender( new ClientContext('sdk-key', basicConfig, { ...mockPlatform, info }), + { + authorization: 'sdk-key', + 'user-agent': 'TestUserAgent/2.0.2', + 'x-launchdarkly-tags': 'application-id/testApplication1 application-version/1.0.0', + 'x-launchdarkly-wrapper': 'Rapper/1.2.3', + }, ); eventSenderResult = await eventSender.sendEventData( diff --git a/packages/shared/common/src/internal/events/EventSender.ts b/packages/shared/common/src/internal/events/EventSender.ts index afee52df19..3d11a14b35 100644 --- a/packages/shared/common/src/internal/events/EventSender.ts +++ b/packages/shared/common/src/internal/events/EventSender.ts @@ -11,7 +11,7 @@ import { LDUnexpectedResponseError, } from '../../errors'; import { ClientContext, getEventsUri } from '../../options'; -import { defaultHeaders, httpErrorMessage, sleep } from '../../utils'; +import { httpErrorMessage, LDHeaders, sleep } from '../../utils'; export default class EventSender implements LDEventSender { private crypto: Crypto; @@ -22,16 +22,14 @@ export default class EventSender implements LDEventSender { private eventsUri: string; private requests: Requests; - constructor(clientContext: ClientContext) { + constructor(clientContext: ClientContext, baseHeaders: LDHeaders) { const { basicConfiguration, platform } = clientContext; const { - sdkKey, - serviceEndpoints: { analyticsEventPath, diagnosticEventPath, includeAuthorizationHeader }, - tags, + serviceEndpoints: { analyticsEventPath, diagnosticEventPath }, } = basicConfiguration; - const { crypto, info, requests } = platform; + const { crypto, requests } = platform; - this.defaultHeaders = defaultHeaders(sdkKey, info, tags, includeAuthorizationHeader); + this.defaultHeaders = { ...baseHeaders }; this.eventsUri = getEventsUri(basicConfiguration.serviceEndpoints, analyticsEventPath, []); this.diagnosticEventsUri = getEventsUri( basicConfiguration.serviceEndpoints, diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index d1c0ac331a..2edb145b91 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -86,6 +86,15 @@ export default class LDClientImpl implements LDClient { this.connectionMode = this.config.initialConnectionMode; this.clientContext = new ClientContext(sdkKey, this.config, platform); this.logger = this.config.logger; + + this.baseHeaders = defaultHeaders( + this.sdkKey, + this.platform.info, + this.config.tags, + this.config.serviceEndpoints.includeAuthorizationHeader, + this.config.userAgentHeaderName, + ); + this.flagManager = new FlagManager( this.platform, sdkKey, @@ -97,6 +106,7 @@ export default class LDClientImpl implements LDClient { sdkKey, this.config, platform, + this.baseHeaders, this.diagnosticsManager, !this.isOffline(), ); @@ -112,14 +122,6 @@ export default class LDClientImpl implements LDClient { const ldContext = Context.toLDContext(context); this.emitter.emit('change', ldContext, flagKeys); }); - - this.baseHeaders = defaultHeaders( - this.sdkKey, - this.platform.info, - this.config.tags, - true, - 'x-launchdarkly-user-agent', - ); } /** diff --git a/packages/shared/sdk-client/src/events/createEventProcessor.ts b/packages/shared/sdk-client/src/events/createEventProcessor.ts index 8e9075e7da..6d45036db0 100644 --- a/packages/shared/sdk-client/src/events/createEventProcessor.ts +++ b/packages/shared/sdk-client/src/events/createEventProcessor.ts @@ -1,4 +1,4 @@ -import { ClientContext, internal, Platform } from '@launchdarkly/js-sdk-common'; +import { ClientContext, internal, LDHeaders, Platform } from '@launchdarkly/js-sdk-common'; import Configuration from '../configuration'; @@ -6,6 +6,7 @@ const createEventProcessor = ( clientSideID: string, config: Configuration, platform: Platform, + baseHeaders: LDHeaders, diagnosticsManager?: internal.DiagnosticsManager, start: boolean = false, ): internal.EventProcessor | undefined => { @@ -13,6 +14,7 @@ const createEventProcessor = ( return new internal.EventProcessor( { ...config, eventsCapacity: config.capacity }, new ClientContext(clientSideID, config, platform), + baseHeaders, undefined, diagnosticsManager, start, diff --git a/packages/shared/sdk-server/__tests__/events/EventProcessor.test.ts b/packages/shared/sdk-server/__tests__/events/EventProcessor.test.ts index 41fa4135ef..6ea238ac9c 100644 --- a/packages/shared/sdk-server/__tests__/events/EventProcessor.test.ts +++ b/packages/shared/sdk-server/__tests__/events/EventProcessor.test.ts @@ -199,6 +199,7 @@ describe('given an event processor with diagnostics manager', () => { eventProcessor = new internal.EventProcessor( testConfig, clientContext, + {}, new ContextDeduplicator(config), diagnosticsManager, ); diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 5f2b177640..34d6f02a51 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -158,6 +158,7 @@ export default class LDClientImpl implements LDClient { } this.config = config; this.logger = config.logger; + const baseHeaders = defaultHeaders(sdkKey, platform.info, config.tags); const clientContext = new ClientContext(sdkKey, config, platform); const featureStore = config.featureStoreFactory(clientContext); @@ -178,6 +179,7 @@ export default class LDClientImpl implements LDClient { this.eventProcessor = new internal.EventProcessor( config, clientContext, + baseHeaders, new ContextDeduplicator(config), this.diagnosticsManager, ); @@ -208,7 +210,6 @@ export default class LDClientImpl implements LDClient { }, }; this.evaluator = new Evaluator(this.platform, queries); - const baseHeaders = defaultHeaders(sdkKey, platform.info, config.tags); const listeners = createStreamListeners(dataSourceUpdates, this.logger, { put: () => this.initSuccess(), From a6e112662c0ebed3d4d1282c4eed59541157f16b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:22:19 -0700 Subject: [PATCH 25/25] Set user agent header name. --- packages/sdk/browser/src/BrowserClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sdk/browser/src/BrowserClient.ts b/packages/sdk/browser/src/BrowserClient.ts index bd46e7ec00..6bb99c8b1d 100644 --- a/packages/sdk/browser/src/BrowserClient.ts +++ b/packages/sdk/browser/src/BrowserClient.ts @@ -25,6 +25,7 @@ export class BrowserClient extends LDClientImpl { diagnosticEventPath: `/events/diagnostic/${clientSideId}`, includeAuthorizationHeader: false, highTimeoutThreshold: 5, + userAgentHeaderName: 'x-launchdarkly-user-agent', }); }