44
55namespace LaunchDarkly \Impl \Events ;
66
7- use LaunchDarkly \LDUser ;
7+ use LaunchDarkly \Impl \Model \AttributeReference ;
8+ use LaunchDarkly \LDContext ;
89
910/**
1011 * Internal class that translates analytics events into the format used for sending them to LaunchDarkly.
1415 */
1516class EventSerializer
1617{
17- private bool $ _allAttrsPrivate ;
18- private array $ _privateAttrNames ;
18+ private bool $ _allAttributesPrivate ;
19+ /** @var AttributeReference[] */
20+ private array $ _privateAttributes ;
1921
2022 public function __construct (array $ options )
2123 {
22- $ this ->_allAttrsPrivate = !!($ options ['all_attributes_private ' ] ?? false );
23- $ this ->_privateAttrNames = $ options ['private_attribute_names ' ] ?? [];
24+ $ this ->_allAttributesPrivate = !!($ options ['all_attributes_private ' ] ?? false );
25+
26+ $ allParsedPrivate = [];
27+ foreach ($ options ['private_attribute_names ' ] ?? [] as $ attr ) {
28+ $ parsed = AttributeReference::parse ($ attr );
29+ if ($ parsed ->getError () === null ) {
30+ $ allParsedPrivate [] = $ parsed ;
31+ }
32+ }
33+ $ this ->_privateAttributes = $ allParsedPrivate ;
2434 }
2535
2636 public function serializeEvents (array $ events ): string
@@ -40,62 +50,125 @@ private function filterEvent(array $e): array
4050 {
4151 $ ret = [];
4252 foreach ($ e as $ key => $ value ) {
43- if ($ key == 'user ' ) {
44- $ ret [$ key ] = $ this ->serializeUser ($ value );
53+ if ($ key == 'context ' ) {
54+ $ ret [$ key ] = $ this ->serializeContext ($ value );
4555 } else {
4656 $ ret [$ key ] = $ value ;
4757 }
4858 }
4959 return $ ret ;
5060 }
5161
52- private function filterAttrs ( array $ attrs , array & $ json , ? array $ userPrivateAttrs , array & $ allPrivateAttrs , bool $ stringify ): void
62+ private function serializeContext ( LDContext $ context ): array
5363 {
54- foreach ($ attrs as $ key => $ value ) {
55- if ($ value !== null ) {
56- if ($ this ->_allAttrsPrivate ||
57- (!is_null ($ userPrivateAttrs ) && in_array ($ key , $ userPrivateAttrs )) ||
58- in_array ($ key , $ this ->_privateAttrNames )) {
59- $ allPrivateAttrs [] = $ key ;
60- } else {
61- $ json [$ key ] = $ stringify ? strval ($ value ) : $ value ;
64+ if ($ context ->isMultiple ()) {
65+ $ ret = ['kind ' => 'multi ' ];
66+ for ($ i = 0 ; $ i < $ context ->getIndividualContextCount (); $ i ++) {
67+ $ c = $ context ->getIndividualContext ($ i );
68+ if ($ c !== null ) {
69+ $ ret [$ c ->getKind ()] = $ this ->serializeContextSingleKind ($ c , false );
6270 }
6371 }
72+ return $ ret ;
73+ } else {
74+ return $ this ->serializeContextSingleKind ($ context , true );
75+ }
76+ }
77+
78+ private function serializeContextSingleKind (LDContext $ c , bool $ includeKind ): array
79+ {
80+ $ ret = ['key ' => $ c ->getKey ()];
81+ if ($ includeKind ) {
82+ $ ret ['kind ' ] = $ c ->getKind ();
83+ }
84+ if ($ c ->isAnonymous ()) {
85+ $ ret ['anonymous ' ] = true ;
86+ }
87+ $ redacted = [];
88+ $ allPrivate = $ this ->_privateAttributes ;
89+ if (!$ this ->_allAttributesPrivate ) {
90+ foreach (($ c ->getPrivateAttributes () ?? []) as $ attr ) {
91+ $ parsed = AttributeReference::parse ($ attr );
92+ if ($ parsed ->getError () === null ) {
93+ $ allPrivate [] = $ parsed ;
94+ }
95+ }
96+ }
97+ if ($ c ->getName () !== null && !$ this ->checkWholeAttributePrivate ('name ' , $ allPrivate , $ redacted )) {
98+ $ ret ['name ' ] = $ c ->getName ();
99+ }
100+ foreach ($ c ->getCustomAttributeNames () as $ attr ) {
101+ if (!$ this ->checkWholeAttributePrivate ($ attr , $ allPrivate , $ redacted )) {
102+ $ value = $ c ->get ($ attr );
103+ $ ret [$ attr ] = self ::redactJsonValue (null , $ attr , $ value , $ allPrivate , $ redacted );
104+ }
105+ }
106+ if (count ($ redacted ) !== 0 ) {
107+ $ ret ['_meta ' ] = ['redactedAttributes ' => $ redacted ];
64108 }
109+ return $ ret ;
65110 }
66111
67- private function serializeUser ( LDUser $ user ): array
112+ private function checkWholeAttributePrivate ( string $ attr , array $ allPrivate , array & $ redactedOut ): bool
68113 {
69- $ json = ["key " => strval ($ user ->getKey ())];
70- $ userPrivateAttrs = $ user ->getPrivateAttributeNames ();
71- $ allPrivateAttrs = [];
114+ if ($ this ->_allAttributesPrivate ) {
115+ $ redactedOut [] = $ attr ;
116+ return true ;
117+ }
118+ foreach ($ allPrivate as $ p ) {
119+ if ($ p ->getComponent (0 ) === $ attr && $ p ->getDepth () === 1 ) {
120+ $ redactedOut [] = $ attr ;
121+ return true ;
122+ }
123+ }
124+ return false ;
125+ }
72126
73- $ attrs = [
74- 'secondary ' => $ user ->getSecondary (),
75- 'ip ' => $ user ->getIP (),
76- 'country ' => $ user ->getCountry (),
77- 'email ' => $ user ->getEmail (),
78- 'name ' => $ user ->getName (),
79- 'avatar ' => $ user ->getAvatar (),
80- 'firstName ' => $ user ->getFirstName (),
81- 'lastName ' => $ user ->getLastName ()
82- ];
83- $ this ->filterAttrs ($ attrs , $ json , $ userPrivateAttrs , $ allPrivateAttrs , true );
84- if ($ user ->getAnonymous ()) {
85- $ json ['anonymous ' ] = true ;
86- }
87- $ custom = $ user ->getCustom ();
88- if (!is_null ($ custom ) && !empty ($ user ->getCustom ())) {
89- $ customOut = [];
90- $ this ->filterAttrs ($ custom , $ customOut , $ userPrivateAttrs , $ allPrivateAttrs , false );
91- if ($ customOut ) { // if this is empty, we will return a json array for 'custom' instead of an object
92- $ json ['custom ' ] = $ customOut ;
127+ private static function redactJsonValue (?array $ parentPath , string $ name , mixed $ value , array $ allPrivate , array &$ redactedOut ): mixed
128+ {
129+ if (!is_array ($ value ) || count ($ value ) === 0 ) {
130+ return $ value ;
131+ }
132+ $ ret = [];
133+ $ currentPath = $ parentPath ?? [];
134+ $ currentPath [] = $ name ;
135+ foreach ($ value as $ k => $ v ) {
136+ if (is_int ($ k )) {
137+ // This is a regular array, not an object with string properties-- redactions don't apply. Technically,
138+ // that's not a 100% solid assumption because in PHP, an array could have a mix of int and string keys.
139+ // But that's not true in JSON or in pretty much any other SDK, so there wouldn't really be any clear
140+ // way to apply our redaction logic in that case anyway.
141+ return $ value ;
142+ }
143+ $ wasRedacted = false ;
144+ foreach ($ allPrivate as $ p ) {
145+ if ($ p ->getDepth () !== count ($ currentPath ) + 1 ) {
146+ continue ;
147+ }
148+ if ($ p ->getComponent (count ($ currentPath )) !== $ k ) {
149+ continue ;
150+ }
151+ $ match = true ;
152+ for ($ i = 0 ; $ i < count ($ currentPath ); $ i ++) {
153+ if ($ p ->getComponent ($ i ) !== $ currentPath [$ i ]) {
154+ $ match = false ;
155+ break ;
156+ }
157+ }
158+ if ($ match ) {
159+ $ redactedOut [] = $ p ->getPath ();
160+ $ wasRedacted = true ;
161+ break ;
162+ }
163+ }
164+ if (!$ wasRedacted ) {
165+ $ ret [$ k ] = self ::redactJsonValue ($ currentPath , $ k , $ v , $ allPrivate , $ redactedOut );
93166 }
94167 }
95- if (count ($ allPrivateAttrs ) ) {
96- sort ( $ allPrivateAttrs );
97- $ json [ ' privateAttrs ' ] = $ allPrivateAttrs ;
168+ if (count ($ ret ) === 0 ) {
169+ // Substitute an empty object here, because an empty array would serialize as [] rather than {}
170+ return new \ stdClass () ;
98171 }
99- return $ json ;
172+ return $ ret ;
100173 }
101174}
0 commit comments