Skip to content

feat: usage report transaction IDs and the usage history - #1

Draft
fuzcap wants to merge 2 commits into
mainfrom
feat/billing
Draft

fuzcap wants to merge 2 commits into
mainfrom
feat/billing

Conversation

@fuzcap

@fuzcap fuzcap commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

What does this PR change?

The SDK side of two Kaiten features: idempotent usage reports (transactionId) and the usage history. Server side: kaitencloud/kaiten#16.

  • transactionId. UsageReportInput.TransactionID makes a report idempotent: Kaiten applies it at most once per key, instance and entitlement within its idempotency window (35 days by default). A malformed key fails locally with ErrInvalidTransactionID; an empty one is left out of the body.
  • Instances.ReportEntitlementUsage returns a UsageReportResult: the usage, plus Replayed (Idempotent-Replayed) and MetadataDropped (Kaiten-Metadata-Dropped). ReportEntitlementUsageMetric is now that call without the two flags.
  • Retries, only with a key. A keyed report is retried on transport errors and on 500, 502, 503 and 504: three attempts in all, every one under the same key, with exponential backoff that honours the context. A report without a key is still sent once, because a failed request may have been counted.
  • Conflicts. A 409 wraps ErrTransactionIDReused or ErrThresholdExceeded according to its problem code, alongside the *Error. ReportUsage no longer reports a reused key as a threshold.
  • Older servers. A Kaiten without transactionId refuses the field (the body is closed). The client then resends the report without it and stops sending keys and retrying for its lifetime, logging that once. WithLogger sets the logger; slog.Default() otherwise.
  • Usage history. Instances.ListUsageReports walks a pair's history (From, To, TransactionID, a Limit ceiling). Instances.ExportUsageReports and ExportOrganizationUsageReports stream CSV or NDJSON: the body is returned unread, with the attachment's file name.
  • internal/gen is regenerated from the current Core OpenAPI document, and the report wrapper passes the new 503 problem through.

Testing

task test           # ok (-race -shuffle=on)
task lint           # 0 issues
task deadcode       # nothing unreachable
task release:check  # compatible additions only; suggested v0.1.0
  • Body: transactionId sent only when set.
  • Result flags read from the headers.
  • Retries: 503, then a transport error, then 200 gives three attempts under one key. Without a key: one attempt. Always 502: gives up after three. No retry on 409, 422, 404 or 403. A cancelled context stops waiting.
  • Conflict codes map to the right sentinel, through both ReportEntitlementUsage and ReportUsage.
  • Malformed keys are refused with no request.
  • An older server's 422 on body.transactionId gives a resend without the key, later reports keyless and unretried, and one log line. A newer server's InvalidTransactionId is not mistaken for it.
  • History pages followed by afterSeq, the Limit ceiling, query parameters.
  • Exports: the body streams through with its file name and content type; problems arrive as *Error with their Code; organization filters and a malformed ID option.

task vuln reports standard library advisories of the local Go toolchain (go1.26.3, fixed in 1.26.6), not of this module.

Compatibility

Additive only (gorelease: no incompatible change). Keys need a Kaiten release with transactionId; against an older one the SDK falls back as described above.

Kaiten now makes usage reports idempotent and keeps their history. The SDK
follows.

- internal/gen is regenerated from the current Core OpenAPI document.
- UsageReportInput.TransactionID: Kaiten applies a report at most once per
  key, instance and entitlement. A key is checked before sending
  (ErrInvalidTransactionID) and omitted from the body when empty.
- Instances.ReportEntitlementUsage returns a UsageReportResult: the usage,
  Replayed (Idempotent-Replayed) and MetadataDropped
  (Kaiten-Metadata-Dropped). ReportEntitlementUsageMetric is now that call
  without the two flags.
- Retries: a report with a key is retried on transport errors and on 500,
  502, 503 and 504, three attempts in all under the same key, with
  exponential backoff that honours the context. A report without a key is
  still sent once.
- A conflict wraps ErrTransactionIDReused or ErrThresholdExceeded according
  to its problem code, alongside the *Error. ReportUsage no longer reports a
  reused key as a threshold.
- A server older than transaction IDs refuses the field: the report is sent
  again without it, and the client stops sending keys and retrying for its
  lifetime, logging that once. WithLogger sets the logger; slog.Default()
  otherwise.
- Instances.ListUsageReports walks a pair's history by afterSeq, with
  From/To/TransactionID filters and a Limit ceiling.
- Instances.ExportUsageReports and ExportOrganizationUsageReports stream CSV
  or NDJSON: the body is returned unread, with the attachment's name.
- The report wrapper passes the new 503 problem through.

Signed-off-by: Tom Ribuot <tom.ribuot@kaiten.sh>
fuzcap pushed a commit to kaitencloud/kaiten that referenced this pull request Oct 5, 2026
github.com/kaitencloud/sdk-go moves to the commit that adds usage report
transaction IDs and the usage history (kaitencloud/sdk-go#1). The SDK
coverage test now maps listUsageReports, exportUsageReports and
exportOrganizationUsageReports to it instead of listing them as awaiting
the SDK.

It is a pseudo-version of that pull request's branch: bump it to the tag
once the SDK pull request is merged and released.

Signed-off-by: Tom Ribuot <tom.ribuot@kaiten.sh>
client.Billing covers every billing operation of the Core contract
(kaiten feat/billing at 56738183): settings, capabilities, health, sync and
period closing; subscriptions; invoices, their export and line reports; the
accounting handoff (Claim / Ack); licence prices and the invoice preview; the
add-on catalogue and attachments; vouchers and redemptions; publishable keys
and customer sessions; customers' payment methods and portal; and what a pk_
key and a kst_ session reach. Paginated lists walk every page and have a
...Page variant. *Error carries Retry-After.

internal/gen is regenerated; an overlay names InvoiceHandoff.status's type,
which collided with Invoice.handoffStatus's. The wrappers of the five deletes
and updates the contract now answers 409 for pass it through. The billing
wrappers are listed in the problem and surface coverage tests.

Signed-off-by: Tom Ribuot <tom.ribuot@kaiten.sh>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant