From 5ec1d3edcd2c552c0756ad2eb8124116d54c0ec0 Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 12 Oct 2026 16:55:36 +1000 Subject: [PATCH] Give unnamed perk events a perk.quantity When the reset-credit count moves by more credits than the list names, the leftover goes into one id-less event. Bar Telemetry Spec v1 now has perk.quantity so that event can say how many credits it stands for; without it Bar UI would read it as one. --- Sources/CodexBarCore/BarTelemetry.swift | 8 +++++--- Tests/CodexBarCoreTests/BarTelemetryTests.swift | 7 +++++++ docs/telemetry.md | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Sources/CodexBarCore/BarTelemetry.swift b/Sources/CodexBarCore/BarTelemetry.swift index fa591d3..4fc3f9f 100644 --- a/Sources/CodexBarCore/BarTelemetry.swift +++ b/Sources/CodexBarCore/BarTelemetry.swift @@ -104,10 +104,11 @@ enum BarEvents { return usageDidReset(from: before.utilization, to: limit.utilization) } - static func perkAttributes(id: String?, expiresAt: TimeInterval?) -> [OTLPTelemetryEncoder.Attribute] { + static func perkAttributes(id: String?, expiresAt: TimeInterval?, quantity: Int? = nil) -> [OTLPTelemetryEncoder.Attribute] { var attributes: [OTLPTelemetryEncoder.Attribute] = [.init("perk.kind", "reset_credit")] if let id { attributes.append(.init("perk.id", id)) } if let expiresAt, expiresAt.isFinite { attributes.append(.init("perk.expires_at", Int(expiresAt))) } + if let quantity { attributes.append(.init("perk.quantity", quantity)) } return attributes } @@ -146,8 +147,9 @@ enum BarEvents { kept[id] = nil goneBudget -= 1 } + // Credits that went without a row to name them are one event for the whole count. if goneBudget > 0 { - events.append(.init(name: "bar.perk.gone", attributes: perkAttributes(id: nil, expiresAt: nil))) + events.append(.init(name: "bar.perk.gone", attributes: perkAttributes(id: nil, expiresAt: nil, quantity: goneBudget))) } // Only a rise in the count is a new perk. A capped list can rotate which credits it @@ -162,7 +164,7 @@ enum BarEvents { kept.updateValue(expiresAt, forKey: id) } if appearBudget > 0 { - events.append(.init(name: "bar.perk.available", attributes: perkAttributes(id: nil, expiresAt: nil))) + events.append(.init(name: "bar.perk.available", attributes: perkAttributes(id: nil, expiresAt: nil, quantity: appearBudget))) } // An expiry reported late, or moved, must be the one a later disappearance is judged by. for (id, expiresAt) in availableNow { diff --git a/Tests/CodexBarCoreTests/BarTelemetryTests.swift b/Tests/CodexBarCoreTests/BarTelemetryTests.swift index fd266e0..b65ffcf 100644 --- a/Tests/CodexBarCoreTests/BarTelemetryTests.swift +++ b/Tests/CodexBarCoreTests/BarTelemetryTests.swift @@ -244,6 +244,13 @@ final class BarTelemetryTests: XCTestCase { let countOnly = BarEvents.perkEvents(previous: ObservedPerks(count: 1, available: [:]), summary: summary(3, nil), now: now) XCTAssertEqual(countOnly.events.map(\.name), ["bar.perk.available"]) + // An unnamed event stands for every credit the count moved by; a named one is one credit. + XCTAssertTrue(countOnly.events[0].attributes.contains { $0.key == "perk.quantity" && $0.value.intValue == "2" }) + XCTAssertFalse(first.events.contains { $0.attributes.contains { $0.key == "perk.quantity" } }) + let countDrop = BarEvents.perkEvents(previous: ObservedPerks(count: 3, available: ["a": 2_000]), summary: summary(0, nil), now: now) + XCTAssertEqual(countDrop.events.map(\.name), ["bar.perk.gone", "bar.perk.gone"]) + XCTAssertFalse(countDrop.events[0].attributes.contains { $0.key == "perk.quantity" }) + XCTAssertTrue(countDrop.events[1].attributes.contains { $0.key == "perk.quantity" && $0.value.intValue == "2" }) } func testPerkGaugeCountsAvailableCreditsWithEarliestExpiry() throws { diff --git a/docs/telemetry.md b/docs/telemetry.md index 8ab508b..cdacf79 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -54,6 +54,7 @@ Codex reset credits are `perk.kind=reset_credit`. - A credit that Codex reports as redeemed or redeeming produces `bar.perk.gone` with `perk.gone.reason=used`. - When the count drops for any other reason, CodexBar reports the missing credit as gone. The reason is `expired` if its expiry has passed, and left out otherwise. - A credit missing from a list while the count holds is not treated as gone, because Codex can return a capped list. +- If the count moves by more credits than CodexBar can name from the list, the rest go into one `bar.perk.available` or `bar.perk.gone` event with no `perk.id`. Its `perk.quantity` is how many credits it covers. Events for a named credit leave `perk.quantity` out, which means 1. ## Extra `codex.*` records