Skip to content

Commit 92f0115

Browse files
committed
refactor(ffe): address review — typed args, dirty-guarded tag writes, simpler UTF-8 truncation, drop dead clear()
1 parent 2cb81e1 commit 92f0115

3 files changed

Lines changed: 40 additions & 47 deletions

File tree

src/api/FeatureFlags/SpanEnrichmentAccumulator.php

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -37,31 +37,33 @@ final class SpanEnrichmentAccumulator
3737
/** @var array<string, string> flagKey => stringified default value. */
3838
private $defaults = array();
3939

40+
/** @var bool Whether the state changed since the last toSpanTags() encode. */
41+
private $dirty = false;
42+
4043
/**
4144
* Record a serial id seen during evaluation. Deduped via a set; dropped
4245
* (with no error) once the frozen cap is reached.
4346
*/
44-
public function addSerialId($id)
47+
public function addSerialId(int $id)
4548
{
46-
$id = (int) $id;
4749
if (isset($this->serialIds[$id])) {
4850
return;
4951
}
5052
if (count($this->serialIds) >= self::MAX_SERIAL_IDS) {
5153
return;
5254
}
5355
$this->serialIds[$id] = true;
56+
$this->dirty = true;
5457
}
5558

5659
/**
5760
* Associate a serial id with a (hashed) subject. The targeting key is
5861
* SHA256-hashed before storage (privacy: raw targeting keys are never
5962
* emitted) and is only recorded when `do_log` authorizes it.
6063
*/
61-
public function addSubject($targetingKey, $id)
64+
public function addSubject(string $targetingKey, int $id)
6265
{
63-
$id = (int) $id;
64-
$hashed = $this->hashTargetingKey((string) $targetingKey);
66+
$hashed = $this->hashTargetingKey($targetingKey);
6567

6668
if (isset($this->subjects[$hashed])) {
6769
if (isset($this->subjects[$hashed][$id])) {
@@ -71,13 +73,15 @@ public function addSubject($targetingKey, $id)
7173
return;
7274
}
7375
$this->subjects[$hashed][$id] = true;
76+
$this->dirty = true;
7477
return;
7578
}
7679

7780
if (count($this->subjects) >= self::MAX_SUBJECTS) {
7881
return;
7982
}
8083
$this->subjects[$hashed] = array($id => true);
84+
$this->dirty = true;
8185
}
8286

8387
/**
@@ -88,9 +92,8 @@ public function addSubject($targetingKey, $id)
8892
*
8993
* @param mixed $value
9094
*/
91-
public function addDefault($flagKey, $value)
95+
public function addDefault(string $flagKey, $value)
9296
{
93-
$flagKey = (string) $flagKey;
9497
if (array_key_exists($flagKey, $this->defaults)) {
9598
return;
9699
}
@@ -99,6 +102,19 @@ public function addDefault($flagKey, $value)
99102
}
100103

101104
$this->defaults[$flagKey] = $this->stringifyDefault($value);
105+
$this->dirty = true;
106+
}
107+
108+
/**
109+
* Whether the accumulated state changed since the last call, consuming the
110+
* flag. Lets the caller skip re-encoding and re-writing the span tags when
111+
* an evaluation added nothing new.
112+
*/
113+
public function consumeDirty(): bool
114+
{
115+
$wasDirty = $this->dirty;
116+
$this->dirty = false;
117+
return $wasDirty;
102118
}
103119

104120
/**
@@ -148,17 +164,6 @@ public function toSpanTags()
148164
return $tags;
149165
}
150166

151-
/**
152-
* Reset all accumulated state. Called after the tags are flushed onto the
153-
* root span so a reused accumulator never leaks across spans/requests.
154-
*/
155-
public function clear()
156-
{
157-
$this->serialIds = array();
158-
$this->subjects = array();
159-
$this->defaults = array();
160-
}
161-
162167
/**
163168
* ULEB128 delta-varint + base64 encoder (frozen).
164169
*
@@ -300,16 +305,15 @@ private function stringifyFloat($value)
300305

301306
/**
302307
* Truncate to at most $maxLength characters without splitting a multi-byte
303-
* UTF-8 sequence. Falls back to a byte-safe trim if the multibyte helpers
304-
* are unavailable.
308+
* UTF-8 sequence. Matches up to $maxLength code points from the start: /u
309+
* treats the subject as UTF-8 and /s lets '.' span newlines. If the subject
310+
* is not valid UTF-8 (preg_match returns false), falls back to a
311+
* codepoint-safe byte walk.
305312
*/
306313
private function truncateUtf8($value, $maxLength)
307314
{
308-
if (function_exists('mb_substr') && function_exists('mb_strlen')) {
309-
if (mb_strlen($value, 'UTF-8') <= $maxLength) {
310-
return $value;
311-
}
312-
return mb_substr($value, 0, $maxLength, 'UTF-8');
315+
if (preg_match('/\A.{0,' . (int) $maxLength . '}/su', $value, $m)) {
316+
return $m[0];
313317
}
314318

315319
return $this->truncateUtf8ByteFallback($value, $maxLength);

src/api/FeatureFlags/SpanEnrichmentRegistry.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ final class SpanEnrichmentRegistry
3939
const CONFIG_KEY = 'DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED';
4040

4141
/**
42-
* Whether the experimental span-enrichment gate is on. Process-level env
43-
* config; cannot toggle mid-request.
42+
* Whether the experimental span-enrichment gate is on.
4443
*
4544
* @return bool
4645
*/
@@ -80,11 +79,12 @@ public static function record($flagKey, $details, $targetingKey)
8079
ObjectKVStore::put($root, self::ACCUMULATOR_KEY, $accumulator);
8180
}
8281

82+
// getExposureData() always returns an array (EvaluationDetails types it).
8383
$exposure = $details->getExposureData();
84-
$serialId = is_array($exposure) && array_key_exists(self::SERIAL_ID_METADATA_KEY, $exposure)
84+
$serialId = array_key_exists(self::SERIAL_ID_METADATA_KEY, $exposure)
8585
? $exposure[self::SERIAL_ID_METADATA_KEY]
8686
: null;
87-
$doLog = is_array($exposure) && !empty($exposure[self::DO_LOG_METADATA_KEY]);
87+
$doLog = !empty($exposure[self::DO_LOG_METADATA_KEY]);
8888

8989
if ($serialId !== null) {
9090
$accumulator->addSerialId((int) $serialId);
@@ -98,11 +98,13 @@ public static function record($flagKey, $details, $targetingKey)
9898
}
9999
}
100100

101-
// Write (overwrite) the encoded union onto the root span's meta. Safe
102-
// to do on every evaluation: toSpanTags() re-encodes the accumulator's
103-
// full state, so the latest write is always the complete union.
104-
foreach ($accumulator->toSpanTags() as $key => $value) {
105-
$root->meta[$key] = $value;
101+
// Only re-encode + rewrite the tags when this evaluation actually
102+
// changed the accumulated state; a repeated/duplicate evaluation adds
103+
// nothing new and the meta already holds the current union.
104+
if ($accumulator->consumeDirty()) {
105+
foreach ($accumulator->toSpanTags() as $key => $value) {
106+
$root->meta[$key] = $value;
107+
}
106108
}
107109
} catch (\Throwable $e) {
108110
// Enrichment must never break flag evaluation.

tests/OpenFeature/SpanEnrichmentAccumulatorTest.php

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -308,17 +308,4 @@ public function testFlagsTagIsBareBase64NotJson(): void
308308
self::assertSame($flags, base64_encode(base64_decode($flags, true)));
309309
}
310310

311-
public function testClearResetsState(): void
312-
{
313-
$acc = new SpanEnrichmentAccumulator();
314-
$acc->addSerialId(1);
315-
$acc->addDefault('flag', 'value');
316-
$acc->addSubject('user', 1);
317-
318-
$acc->clear();
319-
320-
self::assertFalse($acc->hasData());
321-
self::assertSame([], $acc->toSpanTags());
322-
}
323-
324311
}

0 commit comments

Comments
 (0)