@@ -84,13 +84,16 @@ public function setup(): void
8484 /**
8585 * Add metrics in batch (raw append).
8686 *
87+ * Callers must explicitly pass the metric type so event and gauge
88+ * writes are never confused at the call site.
89+ *
8790 * @param array<array{metric: string, value: int, tags?: array<string,mixed>}> $metrics
8891 * @param string $type Metric type: 'event' or 'gauge'
8992 * @param int $batchSize Maximum number of metrics per INSERT statement
9093 * @return bool
9194 * @throws \Exception
9295 */
93- public function addBatch (array $ metrics , string $ type = self :: TYPE_EVENT , int $ batchSize = 1000 ): bool
96+ public function addBatch (array $ metrics , string $ type , int $ batchSize = 1000 ): bool
9497 {
9598 return $ this ->adapter ->addBatch ($ metrics , $ type , $ batchSize );
9699 }
@@ -183,14 +186,23 @@ public function count(array $queries = [], ?string $type = null): int
183186 /**
184187 * Sum metric values using Query objects.
185188 *
189+ * Defaults to events because summing gauges (point-in-time snapshots)
190+ * is semantically meaningless — it averages/accumulates snapshots rather
191+ * than producing a useful total. Callers that truly want a gauge sum
192+ * must opt in explicitly.
193+ *
186194 * @param array<\Utopia\Query\Query> $queries
187195 * @param string $attribute Attribute to sum (default: 'value')
188- * @param string|null $type Metric type: 'event', 'gauge', or null (sum both)
196+ * @param string $type Metric type: 'event' or 'gauge'
189197 * @return int
190198 * @throws \Exception
191199 */
192- public function sum (array $ queries = [], string $ attribute = 'value ' , ? string $ type = null ): int
200+ public function sum (array $ queries = [], string $ attribute = 'value ' , string $ type = self :: TYPE_EVENT ): int
193201 {
202+ if ($ type !== self ::TYPE_EVENT && $ type !== self ::TYPE_GAUGE ) {
203+ throw new \InvalidArgumentException ("Invalid type ' {$ type }'. Allowed: " . self ::TYPE_EVENT . ', ' . self ::TYPE_GAUGE );
204+ }
205+
194206 return $ this ->adapter ->sum ($ queries , $ attribute , $ type );
195207 }
196208
@@ -199,6 +211,9 @@ public function sum(array $queries = [], string $attribute = 'value', ?string $t
199211 *
200212 * Queries the SummingMergeTree daily MV for fast billing/analytics.
201213 *
214+ * Note: Daily MV only stores event metrics. This method always queries
215+ * the daily events table — gauges are never pre-aggregated.
216+ *
202217 * @param array<\Utopia\Query\Query> $queries
203218 * @return array<Metric>
204219 * @throws \Exception
@@ -214,6 +229,9 @@ public function findDaily(array $queries = []): array
214229 * Use this for billing queries — reads pre-aggregated daily rows
215230 * instead of scanning billions of raw events.
216231 *
232+ * Note: Daily MV only stores event metrics. This method always queries
233+ * the daily events table — gauges are never pre-aggregated.
234+ *
217235 * @param array<\Utopia\Query\Query> $queries
218236 * @param string $attribute Attribute to sum (default: 'value')
219237 * @return int
@@ -227,6 +245,9 @@ public function sumDaily(array $queries = [], string $attribute = 'value'): int
227245 /**
228246 * Sum multiple event metrics from the pre-aggregated daily table in one query.
229247 *
248+ * Note: Daily MV only stores event metrics. This method always queries
249+ * the daily events table — gauges are never pre-aggregated.
250+ *
230251 * @param array<string> $metrics List of metric names
231252 * @param array<\Utopia\Query\Query> $queries Additional filters (e.g. date range)
232253 * @return array<string, int> Metric name => sum value
@@ -302,7 +323,7 @@ public function collect(string $metric, int $value, string $type, array $tags =
302323 $ tagsHash = !empty ($ tags ) ? md5 (json_encode ($ tags , JSON_THROW_ON_ERROR )) : '' ;
303324 $ key = $ metric . ': ' . $ type . ': ' . $ tagsHash ;
304325
305- if ($ type === ' event ' ) {
326+ if ($ type === self :: TYPE_EVENT ) {
306327 // Additive: sum values for the same metric + tags combination
307328 if (isset ($ this ->buffer [$ key ])) {
308329 $ this ->buffer [$ key ]['value ' ] += $ value ;
@@ -333,9 +354,15 @@ public function collect(string $metric, int $value, string $type, array $tags =
333354 * Flush the in-memory buffer to storage.
334355 *
335356 * Separates buffered metrics into events and gauges, then writes each batch
336- * to the appropriate table via addBatch().
357+ * to the appropriate table via addBatch(). Only entries whose batch write
358+ * succeeds are removed from the buffer, so a partial failure preserves
359+ * the unwritten metrics for retry on the next flush.
337360 *
338- * @return bool True if flush succeeded (or buffer was empty)
361+ * If addBatch() throws mid-flush, any earlier successful batches have
362+ * already been cleared from the buffer — the exception is allowed to
363+ * propagate so the caller can observe the failure.
364+ *
365+ * @return bool True if all batches succeeded (or buffer was empty)
339366 * @throws \Exception
340367 */
341368 public function flush (): bool
@@ -345,35 +372,51 @@ public function flush(): bool
345372 return true ;
346373 }
347374
348- // Separate events and gauges
375+ // Separate events and gauges; keep track of buffer keys so we can
376+ // selectively unset only the entries whose write succeeded.
377+ $ eventKeys = [];
378+ $ gaugeKeys = [];
349379 $ events = [];
350380 $ gauges = [];
351381
352- foreach ($ this ->buffer as $ entry ) {
382+ foreach ($ this ->buffer as $ key => $ entry ) {
353383 if ($ entry ['type ' ] === self ::TYPE_EVENT ) {
354384 $ events [] = $ entry ;
385+ $ eventKeys [] = $ key ;
355386 } else {
356387 $ gauges [] = $ entry ;
388+ $ gaugeKeys [] = $ key ;
357389 }
358390 }
359391
360- $ result = true ;
392+ $ overallResult = true ;
361393
362- // Flush events to events table
394+ // Flush events — clear buffer entries only on success.
363395 if (!empty ($ events )) {
364- $ result = $ this ->adapter ->addBatch ($ events , self ::TYPE_EVENT );
396+ if ($ this ->adapter ->addBatch ($ events , self ::TYPE_EVENT )) {
397+ foreach ($ eventKeys as $ key ) {
398+ unset($ this ->buffer [$ key ]);
399+ }
400+ } else {
401+ $ overallResult = false ;
402+ }
365403 }
366404
367- // Flush gauges to gauges table
405+ // Flush gauges — clear buffer entries only on success.
368406 if (!empty ($ gauges )) {
369- $ result = $ this ->adapter ->addBatch ($ gauges , self ::TYPE_GAUGE ) && $ result ;
407+ if ($ this ->adapter ->addBatch ($ gauges , self ::TYPE_GAUGE )) {
408+ foreach ($ gaugeKeys as $ key ) {
409+ unset($ this ->buffer [$ key ]);
410+ }
411+ } else {
412+ $ overallResult = false ;
413+ }
370414 }
371415
372- $ this ->buffer = [];
373- $ this ->bufferCount = 0 ;
416+ $ this ->bufferCount = count ($ this ->buffer );
374417 $ this ->lastFlushTime = microtime (true );
375418
376- return $ result ;
419+ return $ overallResult ;
377420 }
378421
379422 /**
0 commit comments