Skip to content

Commit

Permalink
feat: adds datasource status to sdk-client (#590)
Browse files Browse the repository at this point in the history
**Requirements**

- [x] I have added test coverage for new or changed functionality
- [x] I have followed the repository's [pull request submission
guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests)
- [x] I have validated my changes against all supported platform
versions

**Related issues**

SDK-170

**Describe the solution you've provided**

Adds DataSourceStatusManager.
Refactors data source errors into common.
Adds DataSourceErrorKind to classify errors so manager can track state.

---------

Co-authored-by: Ryan Lamb <[email protected]>
  • Loading branch information
tanderson-ld and kinyoklion authored Sep 30, 2024
1 parent 980e4da commit 6f26204
Show file tree
Hide file tree
Showing 27 changed files with 691 additions and 139 deletions.
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';
}
}

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
18 changes: 14 additions & 4 deletions packages/shared/common/src/internal/stream/StreamingProcessor.ts
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

0 comments on commit 6f26204

Please sign in to comment.