Skip to content

Commit 6f44383

Browse files
authored
feat: Add support for Payload Filtering (#551)
1 parent 30b822e commit 6f44383

File tree

23 files changed

+272
-31
lines changed

23 files changed

+272
-31
lines changed

contract-tests/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ app.get('/', (req, res) => {
2727
'all-flags-with-reasons',
2828
'tags',
2929
'big-segments',
30+
'filtering',
31+
'filtering-strict',
3032
'user-type',
3133
'migrations',
3234
'event-sampling',

contract-tests/sdkClientEntity.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,17 @@ export function makeSdkConfig(options, tag) {
2626
if (options.streaming) {
2727
cf.streamUri = options.streaming.baseUri;
2828
cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs);
29+
if (options.streaming.filter) {
30+
cf.payloadFilterKey = options.streaming.filter;
31+
}
2932
}
3033
if (options.polling) {
3134
cf.stream = false;
3235
cf.baseUri = options.polling.baseUri;
3336
cf.pollInterface = options.polling.pollIntervalMs / 1000;
37+
if (options.polling.filter) {
38+
cf.payloadFilterKey = options.polling.filter;
39+
}
3440
}
3541
if (options.events) {
3642
cf.allAttributesPrivate = options.events.allAttributesPrivate;

packages/shared/common/__tests__/options/ServiceEndpoints.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import ServiceEndpoints from '../../src/options/ServiceEndpoints';
1+
import ServiceEndpoints, {
2+
getEventsUri,
3+
getPollingUri,
4+
getStreamingUri,
5+
} from '../../src/options/ServiceEndpoints';
26

37
describe.each([
48
[
@@ -33,3 +37,26 @@ describe.each([
3337
expect(endpoints.events).toEqual(expected.eventsUri);
3438
});
3539
});
40+
41+
it('applies payload filter to polling and streaming endpoints', () => {
42+
const endpoints = new ServiceEndpoints(
43+
'https://stream.launchdarkly.com',
44+
'https://sdk.launchdarkly.com',
45+
'https://events.launchdarkly.com',
46+
'/bulk',
47+
'/diagnostic',
48+
true,
49+
'filterKey',
50+
);
51+
52+
expect(getStreamingUri(endpoints, '/all', [])).toEqual(
53+
'https://stream.launchdarkly.com/all?filter=filterKey',
54+
);
55+
expect(getPollingUri(endpoints, '/sdk/latest-all', [])).toEqual(
56+
'https://sdk.launchdarkly.com/sdk/latest-all?filter=filterKey',
57+
);
58+
expect(
59+
getPollingUri(endpoints, '/sdk/latest-all', [{ key: 'withReasons', value: 'true' }]),
60+
).toEqual('https://sdk.launchdarkly.com/sdk/latest-all?withReasons=true&filter=filterKey');
61+
expect(getEventsUri(endpoints, '/bulk', [])).toEqual('https://events.launchdarkly.com/bulk');
62+
});

packages/shared/common/src/internal/events/EventSender.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
isHttpRecoverable,
1111
LDUnexpectedResponseError,
1212
} from '../../errors';
13-
import { ClientContext } from '../../options';
13+
import { ClientContext, getEventsUri } from '../../options';
1414
import { defaultHeaders, httpErrorMessage, sleep } from '../../utils';
1515

1616
export default class EventSender implements LDEventSender {
@@ -26,19 +26,18 @@ export default class EventSender implements LDEventSender {
2626
const { basicConfiguration, platform } = clientContext;
2727
const {
2828
sdkKey,
29-
serviceEndpoints: {
30-
events,
31-
analyticsEventPath,
32-
diagnosticEventPath,
33-
includeAuthorizationHeader,
34-
},
29+
serviceEndpoints: { analyticsEventPath, diagnosticEventPath, includeAuthorizationHeader },
3530
tags,
3631
} = basicConfiguration;
3732
const { crypto, info, requests } = platform;
3833

3934
this.defaultHeaders = defaultHeaders(sdkKey, info, tags, includeAuthorizationHeader);
40-
this.eventsUri = `${events}${analyticsEventPath}`;
41-
this.diagnosticEventsUri = `${events}${diagnosticEventPath}`;
35+
this.eventsUri = getEventsUri(basicConfiguration.serviceEndpoints, analyticsEventPath, []);
36+
this.diagnosticEventsUri = getEventsUri(
37+
basicConfiguration.serviceEndpoints,
38+
diagnosticEventPath,
39+
[],
40+
);
4241
this.requests = requests;
4342
this.crypto = crypto;
4443
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ describe('given a stream processor with mock event source', () => {
107107
platform: basicPlatform,
108108
},
109109
'/all',
110+
[],
110111
listeners,
111112
diagnosticsManager,
112113
mockErrorHandler,
@@ -142,6 +143,7 @@ describe('given a stream processor with mock event source', () => {
142143
platform: basicPlatform,
143144
},
144145
'/all',
146+
[],
145147
listeners,
146148
diagnosticsManager,
147149
mockErrorHandler,

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { LDStreamProcessor } from '../../api/subsystem';
1010
import { LDStreamingError } from '../../errors';
1111
import { ClientContext } from '../../options';
12+
import { getStreamingUri } from '../../options/ServiceEndpoints';
1213
import { defaultHeaders, httpErrorMessage, shouldRetry } from '../../utils';
1314
import { DiagnosticsManager } from '../diagnostics';
1415
import { StreamingErrorHandler } from './types';
@@ -37,6 +38,7 @@ class StreamingProcessor implements LDStreamProcessor {
3738
sdkKey: string,
3839
clientContext: ClientContext,
3940
streamUriPath: string,
41+
parameters: { key: string; value: string }[],
4042
private readonly listeners: Map<EventName, ProcessStreamResponse>,
4143
private readonly diagnosticsManager?: DiagnosticsManager,
4244
private readonly errorHandler?: StreamingErrorHandler,
@@ -49,7 +51,11 @@ class StreamingProcessor implements LDStreamProcessor {
4951
this.headers = defaultHeaders(sdkKey, info, tags);
5052
this.logger = logger;
5153
this.requests = requests;
52-
this.streamUri = `${basicConfiguration.serviceEndpoints.streaming}${streamUriPath}`;
54+
this.streamUri = getStreamingUri(
55+
basicConfiguration.serviceEndpoints,
56+
streamUriPath,
57+
parameters,
58+
);
5359
}
5460

5561
private logConnectionStarted() {

packages/shared/common/src/options/ServiceEndpoints.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ function canonicalizeUri(uri: string): string {
22
return uri.replace(/\/+$/, '');
33
}
44

5+
function canonicalizePath(path: string): string {
6+
return path.replace(/^\/+/, '').replace(/\?$/, '');
7+
}
8+
59
/**
610
* Specifies the base service URIs used by SDK components.
711
*/
@@ -11,6 +15,7 @@ export default class ServiceEndpoints {
1115
public readonly streaming: string;
1216
public readonly polling: string;
1317
public readonly events: string;
18+
public readonly payloadFilterKey?: string;
1419

1520
/** Valid paths are:
1621
* /bulk
@@ -36,12 +41,84 @@ export default class ServiceEndpoints {
3641
analyticsEventPath: string = '/bulk',
3742
diagnosticEventPath: string = '/diagnostic',
3843
includeAuthorizationHeader: boolean = true,
44+
payloadFilterKey?: string,
3945
) {
4046
this.streaming = canonicalizeUri(streaming);
4147
this.polling = canonicalizeUri(polling);
4248
this.events = canonicalizeUri(events);
4349
this.analyticsEventPath = analyticsEventPath;
4450
this.diagnosticEventPath = diagnosticEventPath;
4551
this.includeAuthorizationHeader = includeAuthorizationHeader;
52+
this.payloadFilterKey = payloadFilterKey;
4653
}
4754
}
55+
56+
function getWithParams(uri: string, parameters: { key: string; value: string }[]) {
57+
if (parameters.length === 0) {
58+
return uri;
59+
}
60+
61+
const parts = parameters.map(({ key, value }) => `${key}=${value}`);
62+
return `${uri}?${parts.join('&')}`;
63+
}
64+
65+
/**
66+
* Get the URI for the streaming endpoint.
67+
*
68+
* @param endpoints The service endpoints.
69+
* @param path The path to the resource, devoid of any query parameters or hrefs.
70+
* @param parameters The query parameters. These query parameters must already have the appropriate encoding applied. This function WILL NOT apply it for you.
71+
*/
72+
export function getStreamingUri(
73+
endpoints: ServiceEndpoints,
74+
path: string,
75+
parameters: { key: string; value: string }[],
76+
): string {
77+
const canonicalizedPath = canonicalizePath(path);
78+
79+
const combinedParameters = [...parameters];
80+
if (endpoints.payloadFilterKey) {
81+
combinedParameters.push({ key: 'filter', value: endpoints.payloadFilterKey });
82+
}
83+
84+
return getWithParams(`${endpoints.streaming}/${canonicalizedPath}`, combinedParameters);
85+
}
86+
87+
/**
88+
* Get the URI for the polling endpoint.
89+
*
90+
* @param endpoints The service endpoints.
91+
* @param path The path to the resource, devoid of any query parameters or hrefs.
92+
* @param parameters The query parameters. These query parameters must already have the appropriate encoding applied. This function WILL NOT apply it for you.
93+
*/
94+
export function getPollingUri(
95+
endpoints: ServiceEndpoints,
96+
path: string,
97+
parameters: { key: string; value: string }[],
98+
): string {
99+
const canonicalizedPath = canonicalizePath(path);
100+
101+
const combinedParameters = [...parameters];
102+
if (endpoints.payloadFilterKey) {
103+
combinedParameters.push({ key: 'filter', value: endpoints.payloadFilterKey });
104+
}
105+
106+
return getWithParams(`${endpoints.polling}/${canonicalizedPath}`, combinedParameters);
107+
}
108+
109+
/**
110+
* Get the URI for the events endpoint.
111+
*
112+
* @param endpoints The service endpoints.
113+
* @param path The path to the resource, devoid of any query parameters or hrefs.
114+
* @param parameters The query parameters. These query parameters must already have the appropriate encoding applied. This function WILL NOT apply it for you.
115+
*/
116+
export function getEventsUri(
117+
endpoints: ServiceEndpoints,
118+
path: string,
119+
parameters: { key: string; value: string }[],
120+
): string {
121+
const canonicalizedPath = canonicalizePath(path);
122+
123+
return getWithParams(`${endpoints.events}/${canonicalizedPath}`, parameters);
124+
}
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import ApplicationTags from './ApplicationTags';
22
import ClientContext from './ClientContext';
33
import OptionMessages from './OptionMessages';
4-
import ServiceEndpoints from './ServiceEndpoints';
4+
import ServiceEndpoints, { getEventsUri, getPollingUri, getStreamingUri } from './ServiceEndpoints';
55

6-
export { ApplicationTags, OptionMessages, ServiceEndpoints, ClientContext };
6+
export {
7+
ApplicationTags,
8+
OptionMessages,
9+
ServiceEndpoints,
10+
ClientContext,
11+
getStreamingUri,
12+
getPollingUri,
13+
getEventsUri,
14+
};

packages/shared/common/src/validators.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export class StringMatchingRegex extends Type<string> {
118118
}
119119

120120
override is(u: unknown): u is string {
121-
return !!(u as string).match(this.expression);
121+
return typeof u === 'string' && !!(u as string).match(this.expression);
122122
}
123123
}
124124

packages/shared/mocks/src/streamingProcessor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const setupMockStreamingProcessor = (
2525
sdkKey: string,
2626
clientContext: ClientContext,
2727
streamUriPath: string,
28+
parameters: { key: string; value: string }[],
2829
listeners: Map<EventName, ProcessStreamResponse>,
2930
diagnosticsManager: internal.DiagnosticsManager,
3031
errorHandler: internal.StreamingErrorHandler,

packages/shared/sdk-client/src/LDClientImpl.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ describe('sdk-client object', () => {
112112
expect.anything(),
113113
'/stream/path',
114114
expect.anything(),
115+
expect.anything(),
115116
undefined,
116117
expect.anything(),
117118
);
@@ -130,7 +131,8 @@ describe('sdk-client object', () => {
130131
expect(MockStreamingProcessor).toHaveBeenCalledWith(
131132
expect.anything(),
132133
expect.anything(),
133-
'/stream/path?withReasons=true',
134+
'/stream/path',
135+
[{ key: 'withReasons', value: 'true' }],
134136
expect.anything(),
135137
undefined,
136138
expect.anything(),

packages/shared/sdk-client/src/LDClientImpl.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -401,15 +401,17 @@ export default class LDClientImpl implements LDClient {
401401
identifyResolve: any,
402402
identifyReject: any,
403403
) {
404-
let pollingPath = this.createPollUriPath(context);
404+
const parameters: { key: string; value: string }[] = [];
405405
if (this.config.withReasons) {
406-
pollingPath = `${pollingPath}?withReasons=true`;
406+
parameters.push({ key: 'withReasons', value: 'true' });
407407
}
408+
408409
this.updateProcessor = new PollingProcessor(
409410
this.sdkKey,
410411
this.clientContext.platform.requests,
411412
this.clientContext.platform.info,
412-
pollingPath,
413+
this.createPollUriPath(context),
414+
parameters,
413415
this.config,
414416
async (flags) => {
415417
this.logger.debug(`Handling polling result: ${Object.keys(flags)}`);
@@ -438,15 +440,16 @@ export default class LDClientImpl implements LDClient {
438440
identifyResolve: any,
439441
identifyReject: any,
440442
) {
441-
let streamingPath = this.createStreamUriPath(context);
443+
const parameters: { key: string; value: string }[] = [];
442444
if (this.config.withReasons) {
443-
streamingPath = `${streamingPath}?withReasons=true`;
445+
parameters.push({ key: 'withReasons', value: 'true' });
444446
}
445447

446448
this.updateProcessor = new internal.StreamingProcessor(
447449
this.sdkKey,
448450
this.clientContext,
449-
streamingPath,
451+
this.createStreamUriPath(context),
452+
parameters,
450453
this.createStreamListeners(checkedContext, identifyResolve),
451454
this.diagnosticsManager,
452455
(e) => {

packages/shared/sdk-client/src/api/LDOptions.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,20 @@ export interface LDOptions {
224224
* If `wrapperName` is unset, this field will be ignored.
225225
*/
226226
wrapperVersion?: string;
227+
228+
/**
229+
* LaunchDarkly Server SDKs historically downloaded all flag configuration and segments for a particular environment
230+
* during initialization.
231+
*
232+
* For some customers, this is an unacceptably large amount of data, and has contributed to performance issues
233+
* within their products.
234+
*
235+
* Filtered environments aim to solve this problem. By allowing customers to specify subsets of an environment's
236+
* flags using a filter key, SDKs will initialize faster and use less memory.
237+
*
238+
* This payload filter key only applies to the default streaming and polling data sources. It will not affect
239+
* TestData or FileData data sources, nor will it be applied to any data source provided through the featureStore
240+
* config property.
241+
*/
242+
payloadFilterKey?: string;
227243
}

0 commit comments

Comments
 (0)