Skip to content

Commit 93ac9dd

Browse files
LaunchDarklyReleaseBoteli-darklyLaunchDarklyCIbwoskow-ldmsiadak
authored
prepare 5.1.0 release (#98)
## [5.1.0] - 2024-03-19 ### Changed: - Redact anonymous attributes within feature events - Always inline contexts for feature events ### Fixed: - Pin dev version of node to compatible types. ### Removed: - HTTP fallback ping --------- Co-authored-by: Eli Bishop <[email protected]> Co-authored-by: LaunchDarklyCI <[email protected]> Co-authored-by: Ben Woskow <[email protected]> Co-authored-by: Michael Siadak <[email protected]> Co-authored-by: Ben Woskow <[email protected]> Co-authored-by: Jeff Wen <[email protected]> Co-authored-by: Andrey Krasnov <[email protected]> Co-authored-by: Gavin Whelan <[email protected]> Co-authored-by: LaunchDarklyReleaseBot <[email protected]> Co-authored-by: Louis Chan <[email protected]> Co-authored-by: Louis Chan <[email protected]> Co-authored-by: Zach Davis <[email protected]> Co-authored-by: Ryan Lamb <[email protected]> Co-authored-by: Mateusz Sikora <[email protected]> Co-authored-by: Yusinto Ngadiman <[email protected]> Co-authored-by: Yusinto Ngadiman <[email protected]> Co-authored-by: Ember Stevens <[email protected]> Co-authored-by: Ember Stevens <[email protected]> Co-authored-by: ld-repository-standards[bot] <113625520+ld-repository-standards[bot]@users.noreply.github.com> Co-authored-by: Kane Parkinson <[email protected]> Co-authored-by: Matthew M. Keeler <[email protected]>
1 parent 3473a7c commit 93ac9dd

14 files changed

+87
-180
lines changed

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Repository Maintainers
2+
* @launchdarkly/team-sdk

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
## LaunchDarkly overview
66

7-
[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!
7+
[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!
88

99
[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly)
1010

@@ -27,7 +27,7 @@ We encourage pull requests and other contributions from the community. Check out
2727
* Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
2828
* Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
2929
* Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
30-
* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
30+
* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
3131
* Explore LaunchDarkly
3232
* [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information
3333
* [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@babel/runtime": "7.6.3",
2929
"@rollup/plugin-replace": "^2.2.0",
3030
"@types/jest": "^27.4.1",
31+
"@types/node": "12.12.6",
3132
"babel-eslint": "^10.1.0",
3233
"babel-jest": "^25.1.0",
3334
"cross-env": "^5.1.4",

src/ContextFilter.js

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,27 @@ function ContextFilter(config) {
1616
* @param {Object} context
1717
* @returns {string[]} A list of the attributes to filter.
1818
*/
19-
const getAttributesToFilter = context =>
20-
(allAttributesPrivate
19+
const getAttributesToFilter = (context, redactAnonymous) =>
20+
(allAttributesPrivate || (redactAnonymous && context.anonymous)
2121
? Object.keys(context)
2222
: [...privateAttributes, ...((context._meta && context._meta.privateAttributes) || [])]
2323
).filter(attr => !protectedAttributes.some(protectedAttr => AttributeReference.compare(attr, protectedAttr)));
2424

2525
/**
2626
* @param {Object} context
27+
* @param {boolean} redactAnonymous
2728
* @returns {Object} A copy of the context with private attributes removed,
2829
* and the redactedAttributes meta populated.
2930
*/
30-
const filterSingleKind = context => {
31+
const filterSingleKind = (context, redactAnonymous) => {
3132
if (typeof context !== 'object' || context === null || Array.isArray(context)) {
3233
return undefined;
3334
}
3435

35-
const { cloned, excluded } = AttributeReference.cloneExcluding(context, getAttributesToFilter(context));
36+
const { cloned, excluded } = AttributeReference.cloneExcluding(
37+
context,
38+
getAttributesToFilter(context, redactAnonymous)
39+
);
3640
cloned.key = String(cloned.key);
3741
if (excluded.length) {
3842
if (!cloned._meta) {
@@ -57,18 +61,19 @@ function ContextFilter(config) {
5761

5862
/**
5963
* @param {Object} context
64+
* @param {boolean} redactAnonymous
6065
* @returns {Object} A copy of the context with the private attributes removed,
6166
* and the redactedAttributes meta populated for each sub-context.
6267
*/
63-
const filterMultiKind = context => {
68+
const filterMultiKind = (context, redactAnonymous) => {
6469
const filtered = {
6570
kind: context.kind,
6671
};
6772
const contextKeys = Object.keys(context);
6873

6974
for (const contextKey of contextKeys) {
7075
if (contextKey !== 'kind') {
71-
const filteredContext = filterSingleKind(context[contextKey]);
76+
const filteredContext = filterSingleKind(context[contextKey], redactAnonymous);
7277
if (filteredContext) {
7378
filtered[contextKey] = filteredContext;
7479
}
@@ -121,13 +126,13 @@ function ContextFilter(config) {
121126
return filtered;
122127
};
123128

124-
filter.filter = context => {
129+
filter.filter = (context, redactAnonymous = false) => {
125130
if (context.kind === undefined || context.kind === null) {
126-
return filterSingleKind(legacyToSingleKind(context));
131+
return filterSingleKind(legacyToSingleKind(context), redactAnonymous);
127132
} else if (context.kind === 'multi') {
128-
return filterMultiKind(context);
133+
return filterMultiKind(context, redactAnonymous);
129134
} else {
130-
return filterSingleKind(context);
135+
return filterSingleKind(context, redactAnonymous);
131136
}
132137
};
133138

src/EventProcessor.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ function EventProcessor(
5050
if (e.kind === 'identify') {
5151
// identify events always have an inline context
5252
ret.context = contextFilter.filter(e.context);
53+
} else if (e.kind === 'feature') {
54+
// feature events always have an inline context
55+
ret.context = contextFilter.filter(e.context, true);
5356
} else {
5457
ret.contextKeys = getContextKeysFromEvent(e);
5558
delete ret['context'];
@@ -136,8 +139,7 @@ function EventProcessor(
136139
}
137140
queue = [];
138141
logger.debug(messages.debugPostingEvents(eventsToSend.length));
139-
return eventSender.sendEvents(eventsToSend, mainEventsUrl).then(responses => {
140-
const responseInfo = responses && responses[0];
142+
return eventSender.sendEvents(eventsToSend, mainEventsUrl).then(responseInfo => {
141143
if (responseInfo) {
142144
if (responseInfo.serverTime) {
143145
lastKnownPastTime = responseInfo.serverTime;

src/EventSender.js

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,8 @@ const utils = require('./utils');
33
const { v1: uuidv1 } = require('uuid');
44
const { getLDHeaders, transformHeaders } = require('./headers');
55

6-
const MAX_URL_LENGTH = 2000;
7-
86
function EventSender(platform, environmentId, options) {
9-
const imageUrlPath = '/a/' + environmentId + '.gif';
107
const baseHeaders = utils.extend({ 'Content-Type': 'application/json' }, getLDHeaders(platform, options));
11-
const httpFallbackPing = platform.httpFallbackPing; // this will be set for us if we're in the browser SDK
128
const sender = {};
139

1410
function getResponseInfo(result) {
@@ -23,7 +19,11 @@ function EventSender(platform, environmentId, options) {
2319
return ret;
2420
}
2521

26-
sender.sendChunk = (events, url, isDiagnostic, usePost) => {
22+
sender.sendEvents = (events, url, isDiagnostic) => {
23+
if (!platform.httpRequest) {
24+
return Promise.resolve();
25+
}
26+
2727
const jsonBody = JSON.stringify(events);
2828
const payloadId = isDiagnostic ? null : uuidv1();
2929

@@ -55,31 +55,7 @@ function EventSender(platform, environmentId, options) {
5555
});
5656
}
5757

58-
if (usePost) {
59-
return doPostRequest(true).catch(() => {});
60-
} else {
61-
httpFallbackPing && httpFallbackPing(url + imageUrlPath + '?d=' + utils.base64URLEncode(jsonBody));
62-
return Promise.resolve(); // we don't wait for this request to complete, it's just a one-way ping
63-
}
64-
};
65-
66-
sender.sendEvents = function(events, url, isDiagnostic) {
67-
if (!platform.httpRequest) {
68-
return Promise.resolve();
69-
}
70-
const canPost = platform.httpAllowsPost();
71-
let chunks;
72-
if (canPost) {
73-
// no need to break up events into chunks if we can send a POST
74-
chunks = [events];
75-
} else {
76-
chunks = utils.chunkEventsForUrl(MAX_URL_LENGTH - url.length, events);
77-
}
78-
const results = [];
79-
for (let i = 0; i < chunks.length; i++) {
80-
results.push(sender.sendChunk(chunks[i], url, isDiagnostic, canPost));
81-
}
82-
return Promise.all(results);
58+
return doPostRequest(true).catch(() => {});
8359
};
8460

8561
return sender;

src/__tests__/ContextFilter-test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,11 @@ describe('when handling single kind contexts', () => {
283283
expect(uf.filter(anonymousContext)).toEqual(contextWithAllAttrsHidden);
284284
});
285285

286+
it('all attributes are redacted when anonymous', () => {
287+
const uf = ContextFilter({});
288+
expect(uf.filter(anonymousContext, true)).toEqual(contextWithAllAttrsHidden);
289+
});
290+
286291
it('converts non-boolean anonymous to boolean.', () => {
287292
const uf = ContextFilter({});
288293
expect(uf.filter({ kind: 'user', key: 'user', anonymous: 'string' })).toEqual({
@@ -330,6 +335,7 @@ describe('when handling mult-kind contexts', () => {
330335
user: {
331336
key: 'abc',
332337
name: 'alphabet',
338+
anonymous: true,
333339
letters: ['a', 'b', 'c'],
334340
order: 3,
335341
object: {
@@ -342,6 +348,25 @@ describe('when handling mult-kind contexts', () => {
342348
},
343349
};
344350

351+
const orgAndUserContextWithAnonymousRedaction = {
352+
kind: 'multi',
353+
organization: {
354+
key: 'LD',
355+
rocks: true,
356+
name: 'name',
357+
department: {
358+
name: 'sdk',
359+
},
360+
},
361+
user: {
362+
key: 'abc',
363+
anonymous: true,
364+
_meta: {
365+
redactedAttributes: ['/letters', '/name', '/object', '/order'],
366+
},
367+
},
368+
};
369+
345370
const orgAndUserContextAllPrivate = {
346371
kind: 'multi',
347372
organization: {
@@ -352,6 +377,7 @@ describe('when handling mult-kind contexts', () => {
352377
},
353378
user: {
354379
key: 'abc',
380+
anonymous: true,
355381
_meta: {
356382
redactedAttributes: ['/letters', '/name', '/object', '/order'],
357383
},
@@ -373,6 +399,7 @@ describe('when handling mult-kind contexts', () => {
373399
user: {
374400
key: 'abc',
375401
order: 3,
402+
anonymous: true,
376403
object: {
377404
a: 'a',
378405
},
@@ -395,6 +422,7 @@ describe('when handling mult-kind contexts', () => {
395422
user: {
396423
key: 'abc',
397424
name: 'alphabet',
425+
anonymous: true,
398426
order: 3,
399427
object: {
400428
a: 'a',
@@ -415,6 +443,11 @@ describe('when handling mult-kind contexts', () => {
415443
expect(uf.filter(orgAndUserContext)).toEqual(orgAndUserContextAllPrivate);
416444
});
417445

446+
it('it should remove attributes from all anonymous contexts', () => {
447+
const uf = ContextFilter({});
448+
expect(uf.filter(orgAndUserContext, true)).toEqual(orgAndUserContextWithAnonymousRedaction);
449+
});
450+
418451
it('it should apply private attributes from the context to the context.', () => {
419452
const uf = ContextFilter({});
420453
expect(uf.filter(orgAndUserContext)).toEqual(orgAndUserContextIncludedPrivate);

src/__tests__/EventProcessor-test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe.each([
7575
expect(e.value).toEqual(source.value);
7676
expect(e.default).toEqual(source.default);
7777
expect(e.reason).toEqual(source.reason);
78-
checkUserInline(e, source, inlineUser);
78+
expect(e.context).toEqual(inlineUser);
7979
}
8080

8181
function checkCustomEvent(e, source) {
@@ -136,7 +136,7 @@ describe.each([
136136
expect(mockEventSender.calls.length()).toEqual(1);
137137
const output = (await mockEventSender.calls.take()).events;
138138
expect(output.length).toEqual(2);
139-
checkFeatureEvent(output[0], event, false);
139+
checkFeatureEvent(output[0], event, false, eventContext);
140140
checkSummaryEvent(output[1]);
141141
});
142142
});
@@ -159,7 +159,7 @@ describe.each([
159159
expect(mockEventSender.calls.length()).toEqual(1);
160160
const output = (await mockEventSender.calls.take()).events;
161161
expect(output.length).toEqual(2);
162-
checkFeatureEvent(output[0], event, false);
162+
checkFeatureEvent(output[0], event, false, eventContext);
163163
checkSummaryEvent(output[1]);
164164
});
165165
});
@@ -236,7 +236,7 @@ describe.each([
236236
expect(mockEventSender.calls.length()).toEqual(1);
237237
const output = (await mockEventSender.calls.take()).events;
238238
expect(output.length).toEqual(3);
239-
checkFeatureEvent(output[0], e, false);
239+
checkFeatureEvent(output[0], e, false, { ...context, kind: context.kind || 'user' });
240240
checkFeatureEvent(output[1], e, true, { ...context, kind: context.kind || 'user' });
241241
checkSummaryEvent(output[2]);
242242
});

0 commit comments

Comments
 (0)