Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
60aae7c
feat: adds datasource status to sdk-client
tanderson-ld Sep 23, 2024
7ef3b5c
self review comments
tanderson-ld Sep 23, 2024
6ef2c49
lint issues
tanderson-ld Sep 23, 2024
2849c1f
Merge remote-tracking branch 'origin' into ta/sc-249239/data-source-s…
tanderson-ld Sep 24, 2024
6f8c6b5
fixing missing identify resolve
tanderson-ld Sep 24, 2024
ff6dda5
removing unused StoreError
tanderson-ld Sep 24, 2024
c4879f5
updated datasource states. shutdown to be closed and now closed and …
tanderson-ld Sep 25, 2024
d95111f
making change detection tests a little less sensitive
tanderson-ld Sep 25, 2024
1c584d3
Merge remote-tracking branch 'origin' into ta/sc-249239/data-source-s…
tanderson-ld Sep 26, 2024
6028c89
now passing emitter to DataSourceStatusManager and fixing related tests
tanderson-ld Sep 26, 2024
90bbc69
linting
tanderson-ld Sep 26, 2024
a2787a6
fixing lint and incorrect commenting in test
tanderson-ld Sep 26, 2024
ae3f473
fixing missed merge logic and fixing unit tests
tanderson-ld Sep 26, 2024
1c24e1b
remove unused import
tanderson-ld Sep 26, 2024
8dd7590
Merge branch 'main' into ta/sc-249239/data-source-status-rebase
tanderson-ld Sep 26, 2024
47b6b70
fixing imports wip
tanderson-ld Sep 27, 2024
ddeb130
Fix build.
kinyoklion Sep 30, 2024
60ce6c5
Fix common tests.
kinyoklion Sep 30, 2024
7df3547
Merge branch 'main' into ta/sc-249239/data-source-status-rebase
kinyoklion Sep 30, 2024
15adf36
tidy
kinyoklion Sep 30, 2024
11e430c
Browser initializing from poll.
kinyoklion Sep 30, 2024
df41f81
Add error handling for single poll.
kinyoklion Sep 30, 2024
57796af
Documentation and string enum.
kinyoklion Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions packages/sdk/browser/__tests__/BrowserDataManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,19 +205,26 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
it('should load cached flags and continue to poll to complete identify', async () => {
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
const identifyResolve = jest.fn();
const identifyReject = jest.fn();

flagManager.loadCached.mockResolvedValue(true);

await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
let identifyResolve: () => void;
let identifyReject: (err: Error) => void;
await new Promise<void>((resolve) => {
identifyResolve = jest.fn().mockImplementation(() => {
resolve();
});
identifyReject = jest.fn();

// this is the function under test
dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
});

expect(logger.debug).toHaveBeenCalledWith(
'[BrowserDataManager] Identify - Flags loaded from cache. Continuing to initialize via a poll.',
);

expect(flagManager.loadCached).toHaveBeenCalledWith(context);
expect(identifyResolve).toHaveBeenCalled();
expect(identifyResolve!).toHaveBeenCalled();
expect(flagManager.init).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ flagA: { flag: true, version: undefined } }),
Expand All @@ -228,19 +235,25 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
it('should identify from polling when there are no cached flags', async () => {
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
const identifyResolve = jest.fn();
const identifyReject = jest.fn();

flagManager.loadCached.mockResolvedValue(false);
let identifyResolve: () => void;
let identifyReject: (err: Error) => void;
await new Promise<void>((resolve) => {
identifyResolve = jest.fn().mockImplementation(() => {
resolve();
});
identifyReject = jest.fn();

await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
// this is the function under test
dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
});

expect(logger.debug).not.toHaveBeenCalledWith(
'Identify - Flags loaded from cache. Continuing to initialize via a poll.',
);

expect(flagManager.loadCached).toHaveBeenCalledWith(context);
expect(identifyResolve).toHaveBeenCalled();
expect(identifyResolve!).toHaveBeenCalled();
expect(flagManager.init).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ flagA: { flag: true, version: undefined } }),
Expand Down
21 changes: 18 additions & 3 deletions packages/sdk/browser/src/BrowserDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {
BaseDataManager,
Configuration,
Context,
DataSourceErrorKind,
DataSourcePaths,
DataSourceState,
FlagManager,
getPollingUri,
internal,
Expand Down Expand Up @@ -80,11 +82,24 @@ export default class BrowserDataManager extends BaseDataManager {
// TODO: Handle wait for network results in a meaningful way. SDK-707

try {
this.dataSourceStatusManager.requestStateUpdate(DataSourceState.Initializing);
const payload = await requestor.requestPayload();
const listeners = this.createStreamListeners(context, identifyResolve);
const putListener = listeners.get('put');
putListener!.processJson(putListener!.deserializeData(payload));
try {
const listeners = this.createStreamListeners(context, identifyResolve);
const putListener = listeners.get('put');
putListener!.processJson(putListener!.deserializeData(payload));
} catch (e: any) {
this.dataSourceStatusManager.reportError(
DataSourceErrorKind.InvalidData,
e.message ?? 'Could not parse poll response',
);
}
} catch (e: any) {
this.dataSourceStatusManager.reportError(
DataSourceErrorKind.NetworkError,
e.message ?? 'unexpected network error',
e.status,
);
identifyReject(e);
}

Expand Down
15 changes: 15 additions & 0 deletions packages/shared/common/src/datasource/DataSourceErrorKinds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum DataSourceErrorKind {
/// An unexpected error, such as an uncaught exception, further
/// described by the error message.
Unknown = 'UNKNOWN',

/// An I/O error such as a dropped connection.
NetworkError = 'NETWORK_ERROR',

/// The LaunchDarkly service returned an HTTP response with an error
/// status, available in the status code.
ErrorResponse = 'ERROR_RESPONSE',

/// The SDK received malformed data from the LaunchDarkly service.
InvalidData = 'INVALID_DATA',
}
37 changes: 37 additions & 0 deletions packages/shared/common/src/datasource/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable max-classes-per-file */
import { DataSourceErrorKind } from './DataSourceErrorKinds';

export class LDFileDataSourceError extends Error {
constructor(message: string) {
super(message);
this.name = 'LaunchDarklyFileDataSourceError';
}
}

export class LDPollingError extends Error {
public readonly kind: DataSourceErrorKind;
public readonly status?: number;
public readonly recoverable: boolean;

constructor(kind: DataSourceErrorKind, message: string, status?: number, recoverable = true) {
super(message);
this.kind = kind;
this.status = status;
this.name = 'LaunchDarklyPollingError';
this.recoverable = recoverable;
}
}

export class LDStreamingError extends Error {
public readonly kind: DataSourceErrorKind;
public readonly code?: number;
public readonly recoverable: boolean;

constructor(kind: DataSourceErrorKind, message: string, code?: number, recoverable = true) {
super(message);
this.kind = kind;
this.code = code;
this.name = 'LaunchDarklyStreamingError';
this.recoverable = recoverable;
}
}
4 changes: 4 additions & 0 deletions packages/shared/common/src/datasource/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { DataSourceErrorKind } from './DataSourceErrorKinds';
import { LDFileDataSourceError, LDPollingError, LDStreamingError } from './errors';

export { DataSourceErrorKind, LDFileDataSourceError, LDPollingError, LDStreamingError };
27 changes: 0 additions & 27 deletions packages/shared/common/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,6 @@
// more complex, then they could be independent files.
/* eslint-disable max-classes-per-file */

export class LDFileDataSourceError extends Error {
constructor(message: string) {
super(message);
this.name = 'LaunchDarklyFileDataSourceError';
}
}

export class LDPollingError extends Error {
public readonly status?: number;

constructor(message: string, status?: number) {
super(message);
this.status = status;
this.name = 'LaunchDarklyPollingError';
}
}

export class LDStreamingError extends Error {
public readonly code?: number;

constructor(message: string, code?: number) {
super(message);
this.code = code;
this.name = 'LaunchDarklyStreamingError';
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reviewers: These moved to datasource/errors.ts

export class LDUnexpectedResponseError extends Error {
constructor(message: string) {
super(message);
Expand Down
16 changes: 15 additions & 1 deletion packages/shared/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import AttributeReference from './AttributeReference';
import Context from './Context';
import ContextFilter from './ContextFilter';
import {
DataSourceErrorKind,
LDFileDataSourceError,
LDPollingError,
LDStreamingError,
} from './datasource';

export * from './api';
export * from './validators';
Expand All @@ -11,4 +17,12 @@ export * from './utils';
export * as internal from './internal';
export * from './errors';

export { AttributeReference, Context, ContextFilter };
export {
AttributeReference,
Context,
ContextFilter,
DataSourceErrorKind,
LDPollingError,
LDStreamingError,
LDFileDataSourceError,
};
2 changes: 1 addition & 1 deletion packages/shared/common/src/internal/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './context';
export * from './diagnostics';
export * from './evaluation';
export * from './events';
export * from './stream';
export * from './context';
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mock

import { EventName, Info, LDLogger, ProcessStreamResponse } from '../../api';
import { LDStreamProcessor } from '../../api/subsystem';
import { LDStreamingError } from '../../errors';
import { DataSourceErrorKind } from '../../datasource/DataSourceErrorKinds';
import { LDStreamingError } from '../../datasource/errors';
import { defaultHeaders } from '../../utils';
import { DiagnosticsManager } from '../diagnostics';
import StreamingProcessor from './StreamingProcessor';
Expand Down Expand Up @@ -260,7 +261,7 @@ describe('given a stream processor with mock event source', () => {

expect(willRetry).toBeFalsy();
expect(mockErrorHandler).toBeCalledWith(
new LDStreamingError(testError.message, testError.status),
new LDStreamingError(DataSourceErrorKind.Unknown, testError.message, testError.status),
);
expect(logger.error).toBeCalledWith(
expect.stringMatching(new RegExp(`${status}.*permanently`)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
Requests,
} from '../../api';
import { LDStreamProcessor } from '../../api/subsystem';
import { LDStreamingError } from '../../errors';
import { DataSourceErrorKind } from '../../datasource/DataSourceErrorKinds';
import { LDStreamingError } from '../../datasource/errors';
import { ClientContext } from '../../options';
import { getStreamingUri } from '../../options/ServiceEndpoints';
import { httpErrorMessage, LDHeaders, shouldRetry } from '../../utils';
Expand All @@ -22,7 +23,9 @@ const reportJsonError = (
) => {
logger?.error(`Stream received invalid data in "${type}" message`);
logger?.debug(`Invalid JSON follows: ${data}`);
errorHandler?.(new LDStreamingError('Malformed JSON data in event stream'));
errorHandler?.(
new LDStreamingError(DataSourceErrorKind.InvalidData, 'Malformed JSON data in event stream'),
);
};

// TODO: SDK-156 - Move to Server SDK specific location
Expand Down Expand Up @@ -87,7 +90,9 @@ class StreamingProcessor implements LDStreamProcessor {
private retryAndHandleError(err: HttpErrorResponse) {
if (!shouldRetry(err)) {
this.logConnectionResult(false);
this.errorHandler?.(new LDStreamingError(err.message, err.status));
this.errorHandler?.(
new LDStreamingError(DataSourceErrorKind.ErrorResponse, err.message, err.status),
);
this.logger?.error(httpErrorMessage(err, 'streaming request'));
return false;
}
Expand Down Expand Up @@ -142,7 +147,12 @@ class StreamingProcessor implements LDStreamProcessor {
}
processJson(dataJson);
} else {
this.errorHandler?.(new LDStreamingError('Unexpected payload from event stream'));
this.errorHandler?.(
new LDStreamingError(
DataSourceErrorKind.Unknown,
'Unexpected payload from event stream',
),
);
}
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/common/src/internal/stream/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { LDStreamingError } from '../../errors';
import { LDStreamingError } from '../../datasource/errors';

export type StreamingErrorHandler = (err: LDStreamingError) => void;
Loading
Loading