Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions Sources/CodexBarCore/OTLPTelemetryEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,34 @@ public enum OTLPTelemetryEncoder {
barMetrics.append(Metric(name: "bar.limit.resets_at", unit: "s", gauge: Gauge(dataPoints: resetsAt)))
}
if let resets = result.rateLimitResetCredits {
let expiry = (resets.credits ?? []).filter { $0.status == "available" }.compactMap(\.expiresAt).min()
barMetrics.append(Metric(name: "bar.perk.available", unit: "{perk}", gauge: Gauge(dataPoints: [Point(
attributes: BarEvents.perkAttributes(id: nil, expiresAt: expiry), timeUnixNano: timestamp,
asDouble: nil, asInt: String(resets.availableCount)
)])))
// One point per credit so Bar UI sees each expiry. Bar UI sums the points, so any
// credits the list doesn't itemise go in an ID-less point to keep the total right;
// with none at all a single 0 point is how Bar UI sees them run out.
Comment thread
GordonBeeming marked this conversation as resolved.
// `availableCount` is the authority: when the list holds more available rows than it,
// only that many are named, soonest expiry first, so the points never sum past it.
let available: [ResetCredit] = (resets.credits ?? []).filter { $0.status == "available" }
let authoritative = max(resets.availableCount, 0)
let listed: [ResetCredit]
if available.count <= authoritative {
listed = available
} else {
let soonestFirst: [ResetCredit] = available.sorted { (lhs: ResetCredit, rhs: ResetCredit) -> Bool in
let left: Double = lhs.expiresAt ?? .infinity
let right: Double = rhs.expiresAt ?? .infinity
return left < right
}
listed = Array(soonestFirst.prefix(authoritative))
}
var perks = listed.map { credit in
Point(attributes: BarEvents.perkAttributes(id: credit.id, expiresAt: credit.expiresAt), timeUnixNano: timestamp,
asDouble: nil, asInt: "1")
}
let unlisted = max(resets.availableCount - listed.count, 0)
if unlisted > 0 || perks.isEmpty {
perks.append(Point(attributes: BarEvents.perkAttributes(id: nil, expiresAt: nil), timeUnixNano: timestamp,
asDouble: nil, asInt: String(unlisted)))
}
barMetrics.append(Metric(name: "bar.perk.available", unit: "{perk}", gauge: Gauge(dataPoints: perks)))
}
}
if let tokens, !tokens.totals.isEmpty {
Expand Down
69 changes: 62 additions & 7 deletions Tests/CodexBarCoreTests/BarTelemetryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,18 +253,73 @@ final class BarTelemetryTests: XCTestCase {
XCTAssertTrue(countDrop.events[1].attributes.contains { $0.key == "perk.quantity" && $0.value.intValue == "2" })
}

func testPerkGaugeCountsAvailableCreditsWithEarliestExpiry() throws {
let usage = try result(#"""
/// The `bar.perk.available` points as (value, attributes by key), in the order they were written.
private func perkPoints(_ json: String) throws -> [(value: String, attributes: [String: String])] {
let usage = try result(json)
let data = try OTLPTelemetryEncoder.metrics(result: usage, planType: nil, observedAt: nil, capturedAt: Date(), serviceVersion: "t")
let root = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any])
let metrics = try XCTUnwrap(((((root["resourceMetrics"] as? [[String: Any]])?.first?["scopeMetrics"]) as? [[String: Any]])?
.flatMap { ($0["metrics"] as? [[String: Any]]) ?? [] }))
let metric = try XCTUnwrap(metrics.first { $0["name"] as? String == "bar.perk.available" })
let points = try XCTUnwrap((metric["gauge"] as? [String: Any])?["dataPoints"] as? [[String: Any]])
return points.map { point in
var attributes: [String: String] = [:]
for attribute in (point["attributes"] as? [[String: Any]]) ?? [] {
let value = attribute["value"] as? [String: Any]
attributes[attribute["key"] as? String ?? ""] = (value?["stringValue"] ?? value?["intValue"]) as? String
}
return (point["asInt"] as? String ?? "", attributes)
}
}

func testPerkGaugeWritesOnePointPerAvailableCredit() throws {
let points = try perkPoints(#"""
{"rateLimitResetCredits":{"availableCount":2,"credits":[
{"id":"a","status":"available","resetType":"x","grantedAt":1,"expiresAt":2000},
{"id":"b","status":"available","resetType":"x","grantedAt":1,"expiresAt":1500},
{"id":"c","status":"redeemed","resetType":"x","grantedAt":1,"expiresAt":100}]}}
"""#)
let data = try OTLPTelemetryEncoder.metrics(result: usage, planType: nil, observedAt: nil, capturedAt: Date(), serviceVersion: "t")
let text = String(decoding: data, as: UTF8.self)
XCTAssertTrue(text.contains(#""name":"bar.perk.available""#))
XCTAssertTrue(text.contains(#"{"key":"perk.expires_at","value":{"intValue":"1500"}}"#))
XCTAssertTrue(text.contains(#"{"key":"perk.kind","value":{"stringValue":"reset_credit"}}"#))
XCTAssertEqual(points.map(\.value), ["1", "1"])
XCTAssertEqual(points.map { $0.attributes["perk.id"] }, ["a", "b"])
XCTAssertEqual(points.map { $0.attributes["perk.expires_at"] }, ["2000", "1500"])
XCTAssertEqual(points.map { $0.attributes["perk.kind"] }, ["reset_credit", "reset_credit"])
}

func testPerkGaugeKeepsTheTotalWhenTheListIsShorterThanTheCount() throws {
let points = try perkPoints(#"""
{"rateLimitResetCredits":{"availableCount":3,"credits":[
{"id":"a","status":"available","resetType":"x","grantedAt":1,"expiresAt":2000}]}}
"""#)
XCTAssertEqual(points.map(\.value), ["1", "2"])
XCTAssertEqual(points[0].attributes["perk.id"], "a")
XCTAssertNil(points[1].attributes["perk.id"])
XCTAssertNil(points[1].attributes["perk.expires_at"])
}

func testPerkGaugeNeverNamesMoreCreditsThanTheCount() throws {
let points = try perkPoints(#"""
{"rateLimitResetCredits":{"availableCount":1,"credits":[
{"id":"later","status":"available","resetType":"x","grantedAt":1,"expiresAt":3000},
{"id":"sooner","status":"available","resetType":"x","grantedAt":1,"expiresAt":2000}]}}
"""#)
XCTAssertEqual(points.map(\.value), ["1"])
XCTAssertEqual(points[0].attributes["perk.id"], "sooner")
}

func testPerkGaugeWritesZeroWhenTheCountIsZeroEvenIfCreditsAreListed() throws {
let points = try perkPoints(#"""
{"rateLimitResetCredits":{"availableCount":0,"credits":[
{"id":"a","status":"available","resetType":"x","grantedAt":1,"expiresAt":2000}]}}
"""#)
XCTAssertEqual(points.map(\.value), ["0"])
XCTAssertNil(points[0].attributes["perk.id"])
}

func testPerkGaugeStillWritesZeroWhenThereAreNoCredits() throws {
let points = try perkPoints(#"{"rateLimitResetCredits":{"availableCount":0,"credits":[]}}"#)
XCTAssertEqual(points.map(\.value), ["0"])
XCTAssertNil(points[0].attributes["perk.id"])
XCTAssertEqual(points[0].attributes["perk.kind"], "reset_credit")
}

func testTelemetryURLAcceptsAnySubsetAndRejectsTheWholeRequestOnABadValue() throws {
Expand Down
2 changes: 1 addition & 1 deletion docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The count's position and totals live in `~/Library/Application Support/CodexBar/

Codex reset credits are `perk.kind=reset_credit`.

- The `bar.perk.available` gauge holds Codex's available count. `perk.expires_at` is the earliest expiry among the available credits.
- The `bar.perk.available` gauge has one point per available credit, each with value 1, the credit's `perk.id` and its `perk.expires_at`. The points add up to Codex's available count. If the count is higher than the credits the list names, the rest go in one point with no `perk.id`. If the list names more available credits than the count, only that many are written, soonest expiry first. With no credits, the gauge is a single 0 point.
- 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.
Expand Down
Loading