@@ -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 );
0 commit comments