Skip to content

Commit ffcbdcc

Browse files
authored
(U2C #13) update all event logic for U2C (#114)
1 parent 0053143 commit ffcbdcc

File tree

11 files changed

+447
-544
lines changed

11 files changed

+447
-544
lines changed

Makefile

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@ TEMP_TEST_OUTPUT=/tmp/sse-contract-test-service.log
1717
# lookups for properties that are numeric strings.
1818
# - "evaluation/parameterized/prerequisites": Can't pass yet because prerequisite cycle detection is not implemented.
1919
# - "evaluation/parameterized/segment recursion": Haven't yet implemented segment recursion.
20-
# - "events": These test suites will be unavailable until more of the U2C implementation is done.
2120
TEST_HARNESS_PARAMS := $(TEST_HARNESS_PARAMS) \
2221
-skip 'evaluation/bucketing/secondary' \
2322
-skip 'evaluation/parameterized/attribute references/array index is not supported' \
2423
-skip 'evaluation/parameterized/prerequisites' \
25-
-skip 'evaluation/parameterized/segment recursion' \
26-
-skip 'events'
24+
-skip 'evaluation/parameterized/segment recursion'
2725

2826
build-contract-tests:
2927
@cd test-service && composer install --no-progress

src/LaunchDarkly/Impl/Events/EventFactory.php

Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use LaunchDarkly\Impl\Evaluation\EvalResult;
99
use LaunchDarkly\Impl\Model\FeatureFlag;
1010
use LaunchDarkly\Impl\Util;
11-
use LaunchDarkly\LDUser;
11+
use LaunchDarkly\LDContext;
1212

1313
/**
1414
* @ignore
@@ -25,15 +25,15 @@ public function __construct(bool $withReasons)
2525

2626
/**
2727
* @param FeatureFlag $flag
28-
* @param LDUser $user
28+
* @param LDContext $context
2929
* @param EvalResult $result
3030
* @param mixed $default
3131
* @param FeatureFlag|null $prereqOfFlag
3232
* @return mixed[]
3333
*/
3434
public function newEvalEvent(
3535
FeatureFlag $flag,
36-
LDUser $user,
36+
LDContext $context,
3737
EvalResult $result,
3838
mixed $default,
3939
?FeatureFlag $prereqOfFlag = null
@@ -44,7 +44,7 @@ public function newEvalEvent(
4444
'kind' => 'feature',
4545
'creationDate' => Util::currentTimeUnixMillis(),
4646
'key' => $flag->getKey(),
47-
'user' => $user,
47+
'context' => $context,
4848
'variation' => $detail->getVariationIndex(),
4949
'value' => $detail->getValue(),
5050
'default' => $default,
@@ -63,22 +63,19 @@ public function newEvalEvent(
6363
if (($forceReasonTracking || $this->_withReasons)) {
6464
$e['reason'] = $detail->getReason()->jsonSerialize();
6565
}
66-
if ($user->getAnonymous()) {
67-
$e['contextKind'] = 'anonymousUser';
68-
}
6966
return $e;
7067
}
7168

7269
/**
7370
* @return mixed[]
7471
*/
75-
public function newDefaultEvent(FeatureFlag $flag, LDUser $user, EvaluationDetail $detail): array
72+
public function newDefaultEvent(FeatureFlag $flag, LDContext $context, EvaluationDetail $detail): array
7673
{
7774
$e = [
7875
'kind' => 'feature',
7976
'creationDate' => Util::currentTimeUnixMillis(),
8077
'key' => $flag->getKey(),
81-
'user' => $user,
78+
'context' => $context,
8279
'value' => $detail->getValue(),
8380
'default' => $detail->getValue(),
8481
'version' => $flag->getVersion()
@@ -93,82 +90,58 @@ public function newDefaultEvent(FeatureFlag $flag, LDUser $user, EvaluationDetai
9390
if ($this->_withReasons) {
9491
$e['reason'] = $detail->getReason()->jsonSerialize();
9592
}
96-
if ($user->getAnonymous()) {
97-
$e['contextKind'] = 'anonymousUser';
98-
}
9993
return $e;
10094
}
10195

10296
/**
10397
* @return mixed[]
10498
*/
105-
public function newUnknownFlagEvent(string $key, LDUser $user, EvaluationDetail $detail): array
99+
public function newUnknownFlagEvent(string $key, LDContext $context, EvaluationDetail $detail): array
106100
{
107101
$e = [
108102
'kind' => 'feature',
109103
'creationDate' => Util::currentTimeUnixMillis(),
110104
'key' => $key,
111-
'user' => $user,
105+
'context' => $context,
112106
'value' => $detail->getValue(),
113107
'default' => $detail->getValue()
114108
];
115109
// the following properties are handled separately so we don't waste bandwidth on unused keys
116110
if ($this->_withReasons) {
117111
$e['reason'] = $detail->getReason()->jsonSerialize();
118112
}
119-
if ($user->getAnonymous()) {
120-
$e['contextKind'] = 'anonymousUser';
121-
}
122113
return $e;
123114
}
124115

125116
/**
126117
* @return mixed[]
127118
*/
128-
public function newIdentifyEvent(LDUser $user): array
119+
public function newIdentifyEvent(LDContext $context): array
129120
{
130121
return [
131122
'kind' => 'identify',
132123
'creationDate' => Util::currentTimeUnixMillis(),
133-
'key' => strval($user->getKey()),
134-
'user' => $user
124+
'context' => $context
135125
];
136126
}
137127

138128
/**
139-
* @param string $eventName
140-
* @param LDUser $user
141-
* @param mixed $data
142-
* @param int|float|null $metricValue
143-
*
144129
* @return mixed[]
145130
*/
146-
public function newCustomEvent(string $eventName, LDUser $user, mixed $data, int|float|null $metricValue): array
131+
public function newCustomEvent(string $eventName, LDContext $context, mixed $data, int|float|null $metricValue): array
147132
{
148133
$e = [
149134
'kind' => 'custom',
150135
'creationDate' => Util::currentTimeUnixMillis(),
151136
'key' => $eventName,
152-
'user' => $user
137+
'context' => $context
153138
];
154139
if ($data !== null) {
155140
$e['data'] = $data;
156141
}
157142
if ($metricValue !== null) {
158143
$e['metricValue'] = $metricValue;
159144
}
160-
if ($user->getAnonymous()) {
161-
$e['contextKind'] = 'anonymousUser';
162-
}
163145
return $e;
164146
}
165-
166-
private static function contextKind(LDUser $user): string
167-
{
168-
if ($user->getAnonymous()) {
169-
return 'anonymousUser';
170-
} else {
171-
return 'user';
172-
}
173-
}
174147
}

src/LaunchDarkly/Impl/Events/EventSerializer.php

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

55
namespace 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.
@@ -14,13 +15,22 @@
1415
*/
1516
class 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
}

src/LaunchDarkly/Impl/Model/FeatureFlag.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public static function getDecoder(): \Closure
9090
array_map(Prerequisite::getDecoder(), $v['prerequisites'] ?: []),
9191
$v['salt'],
9292
array_map(Target::getDecoder(), $v['targets'] ?: []),
93-
array_map(Target::getDecoder(), ($v['contextTargets'] ?? null) ?: []),
93+
array_map(Target::getDecoder(), $v['contextTargets'] ?? []),
9494
array_map(Rule::getDecoder(), $v['rules'] ?: []),
9595
call_user_func(VariationOrRollout::getDecoder(), $v['fallthrough']),
9696
$v['offVariation'],

src/LaunchDarkly/Impl/Model/Segment.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ public static function getDecoder(): \Closure
5959
$v['version'],
6060
$v['included'] ?: [],
6161
$v['excluded'] ?: [],
62-
array_map(SegmentTarget::getDecoder(), ($v['includedContexts'] ?? null) ?: []),
63-
array_map(SegmentTarget::getDecoder(), ($v['excludedContexts'] ?? null) ?: []),
62+
array_map(SegmentTarget::getDecoder(), $v['includedContexts'] ?? []),
63+
array_map(SegmentTarget::getDecoder(), $v['excludedContexts'] ?? []),
6464
$v['salt'],
6565
array_map(SegmentRule::getDecoder(), $v['rules'] ?: []),
6666
$v['deleted']

0 commit comments

Comments
 (0)