Skip to content

Commit ef919f0

Browse files
authored
Update event format and filtering for contexts. (#59)
1 parent 1922560 commit ef919f0

14 files changed

+749
-531
lines changed

src/ContextFilter.js

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
const AttributeReference = require('./attributeReference');
2+
3+
function ContextFilter(config) {
4+
const filter = {};
5+
6+
const allAttributesPrivate = config.allAttributesPrivate;
7+
const privateAttributes = config.privateAttributes || [];
8+
9+
// These attributes cannot be removed via a private attribute.
10+
const protectedAttributes = ['key', 'kind', '_meta', 'transient'];
11+
12+
const legacyTopLevelCopyAttributes = ['name', 'ip', 'firstName', 'lastName', 'email', 'avatar', 'country'];
13+
14+
/**
15+
* For the given context and configuration get a list of attributes to filter.
16+
* @param {Object} context
17+
* @returns {string[]} A list of the attributes to filter.
18+
*/
19+
const getAttributesToFilter = context =>
20+
(allAttributesPrivate
21+
? Object.keys(context)
22+
: [...privateAttributes, ...((context._meta && context._meta.privateAttributes) || [])]
23+
).filter(attr => !protectedAttributes.some(protectedAttr => AttributeReference.compare(attr, protectedAttr)));
24+
25+
/**
26+
* @param {Object} context
27+
* @returns {Object} A copy of the context with private attributes removed,
28+
* and the redactedAttributes meta populated.
29+
*/
30+
const filterSingleKind = context => {
31+
if (typeof context !== 'object' || context === null || Array.isArray(context)) {
32+
return undefined;
33+
}
34+
35+
const { cloned, excluded } = AttributeReference.cloneExcluding(context, getAttributesToFilter(context));
36+
cloned.key = String(cloned.key);
37+
if (excluded.length) {
38+
if (!cloned._meta) {
39+
cloned._meta = {};
40+
}
41+
cloned._meta.redactedAttributes = excluded;
42+
}
43+
if (cloned._meta) {
44+
if (cloned._meta.secondary === null) {
45+
delete cloned._meta.secondary;
46+
}
47+
if (cloned._meta.secondary !== undefined) {
48+
cloned._meta.secondary = String(cloned._meta.secondary);
49+
}
50+
delete cloned._meta['privateAttributes'];
51+
if (Object.keys(cloned._meta).length === 0) {
52+
delete cloned._meta;
53+
}
54+
}
55+
// Make sure transient is boolean if present.
56+
// Null counts as present, and would be falsy, which is the default.
57+
if (cloned.transient !== undefined) {
58+
cloned.transient = !!cloned.transient;
59+
}
60+
61+
return cloned;
62+
};
63+
64+
/**
65+
* @param {Object} context
66+
* @returns {Object} A copy of the context with the private attributes removed,
67+
* and the redactedAttributes meta populated for each sub-context.
68+
*/
69+
const filterMultiKind = context => {
70+
const filtered = {
71+
kind: context.kind,
72+
};
73+
const contextKeys = Object.keys(context);
74+
75+
for (const contextKey of contextKeys) {
76+
if (contextKey !== 'kind') {
77+
const filteredContext = filterSingleKind(context[contextKey]);
78+
if (filteredContext) {
79+
filtered[contextKey] = filteredContext;
80+
}
81+
}
82+
}
83+
return filtered;
84+
};
85+
86+
/**
87+
* Convert the LDUser object into an LDContext object.
88+
* @param {Object} user The LDUser to produce an LDContext for.
89+
* @returns {Object} A single kind context based on the provided user.
90+
*/
91+
const legacyToSingleKind = user => {
92+
const filtered = {
93+
/* Destructure custom items into the top level.
94+
Duplicate keys will be overridden by previously
95+
top level items.
96+
*/
97+
...(user.custom || {}),
98+
99+
// Implicity a user kind.
100+
kind: 'user',
101+
102+
key: user.key,
103+
};
104+
105+
if (user.anonymous !== undefined) {
106+
filtered.transient = !!user.anonymous;
107+
}
108+
109+
// Copy top level keys and convert them to strings.
110+
// Remove keys that may have been destructured from `custom`.
111+
for (const key of legacyTopLevelCopyAttributes) {
112+
delete filtered[key];
113+
if (user[key] !== undefined && user[key] !== null) {
114+
filtered[key] = String(user[key]);
115+
}
116+
}
117+
118+
if (user.privateAttributeNames !== undefined && user.privateAttributeNames !== null) {
119+
filtered._meta = filtered._meta || {};
120+
// If any private attributes started with '/' we need to convert them to references, otherwise the '/' will
121+
// cause the literal to incorrectly be treated as a reference.
122+
filtered._meta.privateAttributes = user.privateAttributeNames.map(
123+
literal => (literal.startsWith('/') ? AttributeReference.literalToReference(literal) : literal)
124+
);
125+
}
126+
if (user.secondary !== undefined && user.secondary !== null) {
127+
filtered._meta = filtered._meta || {};
128+
filtered._meta.secondary = String(user.secondary);
129+
}
130+
131+
return filtered;
132+
};
133+
134+
filter.filter = context => {
135+
if (context.kind === undefined || context.kind === null) {
136+
return filterSingleKind(legacyToSingleKind(context));
137+
} else if (context.kind === 'multi') {
138+
return filterMultiKind(context);
139+
} else {
140+
return filterSingleKind(context);
141+
}
142+
};
143+
144+
return filter;
145+
}
146+
147+
module.exports = ContextFilter;

src/EventProcessor.js

+30-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const EventSender = require('./EventSender');
22
const EventSummarizer = require('./EventSummarizer');
3-
const UserFilter = require('./UserFilter');
3+
const ContextFilter = require('./ContextFilter');
44
const errors = require('./errors');
55
const messages = require('./messages');
66
const utils = require('./utils');
@@ -17,8 +17,7 @@ function EventProcessor(
1717
const eventSender = sender || EventSender(platform, environmentId, options);
1818
const mainEventsUrl = options.eventsUrl + '/events/bulk/' + environmentId;
1919
const summarizer = EventSummarizer();
20-
const userFilter = UserFilter(options);
21-
const inlineUsers = options.inlineUsersInEvents;
20+
const contextFilter = ContextFilter(options);
2221
const samplingInterval = options.samplingInterval;
2322
const eventCapacity = options.eventCapacity;
2423
const flushInterval = options.flushInterval;
@@ -47,16 +46,12 @@ function EventProcessor(
4746
// Transform an event from its internal format to the format we use when sending a payload.
4847
function makeOutputEvent(e) {
4948
const ret = utils.extend({}, e);
50-
if (e.kind === 'alias') {
51-
// alias events do not require any transformation
52-
return ret;
53-
}
54-
if (inlineUsers || e.kind === 'identify') {
55-
// identify events always have an inline user
56-
ret.user = userFilter.filterUser(e.user);
49+
if (e.kind === 'identify') {
50+
// identify events always have an inline context
51+
ret.context = contextFilter.filter(e.context);
5752
} else {
58-
ret.userKey = e.user.key;
59-
delete ret['user'];
53+
ret.contextKeys = getContextKeys(e);
54+
delete ret['context'];
6055
}
6156
if (e.kind === 'feature') {
6257
delete ret['trackEvents'];
@@ -65,6 +60,28 @@ function EventProcessor(
6560
return ret;
6661
}
6762

63+
function getContextKeys(event) {
64+
const keys = {};
65+
const context = event.context;
66+
if (context !== undefined) {
67+
if (context.kind === undefined) {
68+
keys.user = String(context.key);
69+
} else if (context.kind === 'multi') {
70+
Object.entries(context)
71+
.filter(([key]) => key !== 'kind')
72+
.forEach(([key, value]) => {
73+
if (value !== undefined && value.key !== undefined) {
74+
keys[key] = value.key;
75+
}
76+
});
77+
} else {
78+
keys[context.kind] = String(context.key);
79+
}
80+
return keys;
81+
}
82+
return undefined;
83+
}
84+
6885
function addToOutbox(event) {
6986
if (queue.length < eventCapacity) {
7087
queue.push(event);
@@ -107,7 +124,7 @@ function EventProcessor(
107124
}
108125
if (addDebugEvent) {
109126
const debugEvent = utils.extend({}, event, { kind: 'debug' });
110-
debugEvent.user = userFilter.filterUser(debugEvent.user);
127+
debugEvent.context = contextFilter.filter(debugEvent.context);
111128
delete debugEvent['trackEvents'];
112129
delete debugEvent['debugEventsUntilDate'];
113130
addToOutbox(debugEvent);

src/Identity.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
const utils = require('./utils');
22

3-
function Identity(initialUser, onChange) {
3+
function Identity(initialContext, onChange) {
44
const ident = {};
55
let user;
66

77
ident.setUser = function(u) {
8-
const previousUser = user && utils.clone(user);
9-
user = utils.sanitizeUser(u);
8+
user = utils.sanitizeContext(u);
109
if (user && onChange) {
11-
onChange(utils.clone(user), previousUser);
10+
onChange(utils.clone(user));
1211
}
1312
};
1413

1514
ident.getContext = function() {
1615
return user ? utils.clone(user) : null;
1716
};
1817

19-
if (initialUser) {
20-
ident.setUser(initialUser);
18+
if (initialContext) {
19+
ident.setUser(initialContext);
2120
}
2221

2322
return ident;

src/UserFilter.js

-75
This file was deleted.

0 commit comments

Comments
 (0)