Skip to content

Commit 6f26204

Browse files
feat: adds datasource status to sdk-client (#590)
**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 <4955475+kinyoklion@users.noreply.github.com>
1 parent 980e4da commit 6f26204

27 files changed

+691
-139
lines changed

packages/sdk/browser/__tests__/BrowserDataManager.test.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -205,19 +205,26 @@ describe('given a BrowserDataManager with mocked dependencies', () => {
205205
it('should load cached flags and continue to poll to complete identify', async () => {
206206
const context = Context.fromLDContext({ kind: 'user', key: 'test-user' });
207207
const identifyOptions: LDIdentifyOptions = { waitForNetworkResults: false };
208-
const identifyResolve = jest.fn();
209-
const identifyReject = jest.fn();
210-
211208
flagManager.loadCached.mockResolvedValue(true);
212209

213-
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
210+
let identifyResolve: () => void;
211+
let identifyReject: (err: Error) => void;
212+
await new Promise<void>((resolve) => {
213+
identifyResolve = jest.fn().mockImplementation(() => {
214+
resolve();
215+
});
216+
identifyReject = jest.fn();
217+
218+
// this is the function under test
219+
dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
220+
});
214221

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

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

234-
flagManager.loadCached.mockResolvedValue(false);
239+
let identifyResolve: () => void;
240+
let identifyReject: (err: Error) => void;
241+
await new Promise<void>((resolve) => {
242+
identifyResolve = jest.fn().mockImplementation(() => {
243+
resolve();
244+
});
245+
identifyReject = jest.fn();
235246

236-
await dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
247+
// this is the function under test
248+
dataManager.identify(identifyResolve, identifyReject, context, identifyOptions);
249+
});
237250

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

242255
expect(flagManager.loadCached).toHaveBeenCalledWith(context);
243-
expect(identifyResolve).toHaveBeenCalled();
256+
expect(identifyResolve!).toHaveBeenCalled();
244257
expect(flagManager.init).toHaveBeenCalledWith(
245258
expect.anything(),
246259
expect.objectContaining({ flagA: { flag: true, version: undefined } }),

packages/sdk/browser/src/BrowserDataManager.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import {
22
BaseDataManager,
33
Configuration,
44
Context,
5+
DataSourceErrorKind,
56
DataSourcePaths,
7+
DataSourceState,
68
FlagManager,
79
getPollingUri,
810
internal,
@@ -80,11 +82,24 @@ export default class BrowserDataManager extends BaseDataManager {
8082
// TODO: Handle wait for network results in a meaningful way. SDK-707
8183

8284
try {
85+
this.dataSourceStatusManager.requestStateUpdate(DataSourceState.Initializing);
8386
const payload = await requestor.requestPayload();
84-
const listeners = this.createStreamListeners(context, identifyResolve);
85-
const putListener = listeners.get('put');
86-
putListener!.processJson(putListener!.deserializeData(payload));
87+
try {
88+
const listeners = this.createStreamListeners(context, identifyResolve);
89+
const putListener = listeners.get('put');
90+
putListener!.processJson(putListener!.deserializeData(payload));
91+
} catch (e: any) {
92+
this.dataSourceStatusManager.reportError(
93+
DataSourceErrorKind.InvalidData,
94+
e.message ?? 'Could not parse poll response',
95+
);
96+
}
8797
} catch (e: any) {
98+
this.dataSourceStatusManager.reportError(
99+
DataSourceErrorKind.NetworkError,
100+
e.message ?? 'unexpected network error',
101+
e.status,
102+
);
88103
identifyReject(e);
89104
}
90105

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export enum DataSourceErrorKind {
2+
/// An unexpected error, such as an uncaught exception, further
3+
/// described by the error message.
4+
Unknown = 'UNKNOWN',
5+
6+
/// An I/O error such as a dropped connection.
7+
NetworkError = 'NETWORK_ERROR',
8+
9+
/// The LaunchDarkly service returned an HTTP response with an error
10+
/// status, available in the status code.
11+
ErrorResponse = 'ERROR_RESPONSE',
12+
13+
/// The SDK received malformed data from the LaunchDarkly service.
14+
InvalidData = 'INVALID_DATA',
15+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* eslint-disable max-classes-per-file */
2+
import { DataSourceErrorKind } from './DataSourceErrorKinds';
3+
4+
export class LDFileDataSourceError extends Error {
5+
constructor(message: string) {
6+
super(message);
7+
this.name = 'LaunchDarklyFileDataSourceError';
8+
}
9+
}
10+
11+
export class LDPollingError extends Error {
12+
public readonly kind: DataSourceErrorKind;
13+
public readonly status?: number;
14+
public readonly recoverable: boolean;
15+
16+
constructor(kind: DataSourceErrorKind, message: string, status?: number, recoverable = true) {
17+
super(message);
18+
this.kind = kind;
19+
this.status = status;
20+
this.name = 'LaunchDarklyPollingError';
21+
this.recoverable = recoverable;
22+
}
23+
}
24+
25+
export class LDStreamingError extends Error {
26+
public readonly kind: DataSourceErrorKind;
27+
public readonly code?: number;
28+
public readonly recoverable: boolean;
29+
30+
constructor(kind: DataSourceErrorKind, message: string, code?: number, recoverable = true) {
31+
super(message);
32+
this.kind = kind;
33+
this.code = code;
34+
this.name = 'LaunchDarklyStreamingError';
35+
this.recoverable = recoverable;
36+
}
37+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { DataSourceErrorKind } from './DataSourceErrorKinds';
2+
import { LDFileDataSourceError, LDPollingError, LDStreamingError } from './errors';
3+
4+
export { DataSourceErrorKind, LDFileDataSourceError, LDPollingError, LDStreamingError };

packages/shared/common/src/errors.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,6 @@
22
// more complex, then they could be independent files.
33
/* eslint-disable max-classes-per-file */
44

5-
export class LDFileDataSourceError extends Error {
6-
constructor(message: string) {
7-
super(message);
8-
this.name = 'LaunchDarklyFileDataSourceError';
9-
}
10-
}
11-
12-
export class LDPollingError extends Error {
13-
public readonly status?: number;
14-
15-
constructor(message: string, status?: number) {
16-
super(message);
17-
this.status = status;
18-
this.name = 'LaunchDarklyPollingError';
19-
}
20-
}
21-
22-
export class LDStreamingError extends Error {
23-
public readonly code?: number;
24-
25-
constructor(message: string, code?: number) {
26-
super(message);
27-
this.code = code;
28-
this.name = 'LaunchDarklyStreamingError';
29-
}
30-
}
31-
325
export class LDUnexpectedResponseError extends Error {
336
constructor(message: string) {
347
super(message);

packages/shared/common/src/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import AttributeReference from './AttributeReference';
22
import Context from './Context';
33
import ContextFilter from './ContextFilter';
4+
import {
5+
DataSourceErrorKind,
6+
LDFileDataSourceError,
7+
LDPollingError,
8+
LDStreamingError,
9+
} from './datasource';
410

511
export * from './api';
612
export * from './validators';
@@ -11,4 +17,12 @@ export * from './utils';
1117
export * as internal from './internal';
1218
export * from './errors';
1319

14-
export { AttributeReference, Context, ContextFilter };
20+
export {
21+
AttributeReference,
22+
Context,
23+
ContextFilter,
24+
DataSourceErrorKind,
25+
LDPollingError,
26+
LDStreamingError,
27+
LDFileDataSourceError,
28+
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
export * from './context';
12
export * from './diagnostics';
23
export * from './evaluation';
34
export * from './events';
45
export * from './stream';
5-
export * from './context';

packages/shared/common/src/internal/stream/StreamingProcessor.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mock
22

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

261262
expect(willRetry).toBeFalsy();
262263
expect(mockErrorHandler).toBeCalledWith(
263-
new LDStreamingError(testError.message, testError.status),
264+
new LDStreamingError(DataSourceErrorKind.Unknown, testError.message, testError.status),
264265
);
265266
expect(logger.error).toBeCalledWith(
266267
expect.stringMatching(new RegExp(`${status}.*permanently`)),

packages/shared/common/src/internal/stream/StreamingProcessor.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
Requests,
88
} from '../../api';
99
import { LDStreamProcessor } from '../../api/subsystem';
10-
import { LDStreamingError } from '../../errors';
10+
import { DataSourceErrorKind } from '../../datasource/DataSourceErrorKinds';
11+
import { LDStreamingError } from '../../datasource/errors';
1112
import { ClientContext } from '../../options';
1213
import { getStreamingUri } from '../../options/ServiceEndpoints';
1314
import { httpErrorMessage, LDHeaders, shouldRetry } from '../../utils';
@@ -22,7 +23,9 @@ const reportJsonError = (
2223
) => {
2324
logger?.error(`Stream received invalid data in "${type}" message`);
2425
logger?.debug(`Invalid JSON follows: ${data}`);
25-
errorHandler?.(new LDStreamingError('Malformed JSON data in event stream'));
26+
errorHandler?.(
27+
new LDStreamingError(DataSourceErrorKind.InvalidData, 'Malformed JSON data in event stream'),
28+
);
2629
};
2730

2831
// TODO: SDK-156 - Move to Server SDK specific location
@@ -87,7 +90,9 @@ class StreamingProcessor implements LDStreamProcessor {
8790
private retryAndHandleError(err: HttpErrorResponse) {
8891
if (!shouldRetry(err)) {
8992
this.logConnectionResult(false);
90-
this.errorHandler?.(new LDStreamingError(err.message, err.status));
93+
this.errorHandler?.(
94+
new LDStreamingError(DataSourceErrorKind.ErrorResponse, err.message, err.status),
95+
);
9196
this.logger?.error(httpErrorMessage(err, 'streaming request'));
9297
return false;
9398
}
@@ -142,7 +147,12 @@ class StreamingProcessor implements LDStreamProcessor {
142147
}
143148
processJson(dataJson);
144149
} else {
145-
this.errorHandler?.(new LDStreamingError('Unexpected payload from event stream'));
150+
this.errorHandler?.(
151+
new LDStreamingError(
152+
DataSourceErrorKind.Unknown,
153+
'Unexpected payload from event stream',
154+
),
155+
);
146156
}
147157
});
148158
});

0 commit comments

Comments
 (0)