Skip to content

fix: OpenAPI guardian sweep 2026-09-28 (billable metrics, events) - #586

Open
lago-claude-ai-agent[bot] wants to merge 3 commits into
mainfrom
openapi-guardian/2026-09-28
Open

lago-claude-ai-agent[bot] wants to merge 3 commits into
mainfrom
openapi-guardian/2026-09-28

Conversation

@lago-claude-ai-agent

Copy link
Copy Markdown
Contributor

OpenAPI Guardian sweep — 2026-09-28 (slice 0: billable_metrics, events)

Automated spec sweep vs lago-api and the SDK clients.
A human must review and merge — this agent never merges.
npm run build and npm run test pass on this branch (0 errors; the 23 array-params-plural warnings are pre-existing on main and untouched here).

Slice picked as ISO week 40 % 8 = 0 (weekly cadence for this run).

Fixed in this PR

Type Count
Typos / definitions 9
Required vs optional 0
Inaccuracies (types, nullability, enums, shapes) 3
Filters / query params / paths 1

Field-level evidence

Typos (7)

  • src/schemas/BillableMetricObject.yaml, src/schemas/BillableMetricBaseInput.yaml, src/schemas/BillableMetricEvaluateExpressionInput.yaml — expression description: "evalutated" → "evaluated".
  • src/schemas/BillableMetricBaseInput.yaml, src/schemas/BillableMetricEvaluateExpressionInput.yaml — "Accepted function are" → "Accepted functions are".
  • src/schemas/EventInputObject.yaml timestamp: "miliseconds" → "milliseconds".
  • src/schemas/EventObject.yaml precise_total_amount_cents: "This filed is used by" → "This field is used by".

Definitions (2)

  • src/schemas/EventObject.yaml timestamp: described as "the Unix timestamp in seconds" → described as an ISO 8601 datetime with millisecond precision — evidence: V1::EventSerializer emits model.timestamp.iso8601(3) (app/serializers/v1/event_serializer.rb). The Unix-seconds form is the request format and is already documented correctly on EventInputObject.
  • src/schemas/EventObject.yaml external_subscription_id: dropped the clause "mandatory ... when the external_customer_id is not provided" — evidence: Api::V1::EventsController#create_params does not permit external_customer_id (app/controllers/api/v1/events_controller.rb), so the condition refers to a field the v1 event input no longer accepts.

Inaccuracies (3)

  • src/schemas/BillableMetricObject.yaml aggregation_type: enum gains custom_agg — evidence: BillableMetric::AGGREGATION_TYPES maps custom_agg: 7 (app/models/billable_metric.rb:29-38), V1::BillableMetricSerializer emits the enum name verbatim, and the exports_billable_metrics view in db/structure.sql maps 7 → 'custom_agg'. The API could return a value the spec rejected. Left out of BillableMetricBaseInput deliberately: custom_agg requires custom_aggregator (validates :custom_aggregator, presence: true, if: :custom_agg?), which no permitted param ever assigns, so a custom_agg create/update always 422s. The description now states it is response-only.
  • src/schemas/BillableMetricObject.yaml expression: string → [string, "null"] — evidence: db/structure.sql declares expression character varying with no NOT NULL and no default; the serializer emits the column unconditionally. Both the Python (BillableMetricResponse.expression: Optional[str]) and Rust (expression: Option<String>) clients already model it as nullable.
  • src/schemas/EventEstimateFeesInput.yaml: added precise_total_amount_cents — evidence: estimate_fees runs on create_params, which permits it, and Fees::EstimatePayInAdvanceService reads event_params[:precise_total_amount_cents] || 0 when building the estimated event. The Ruby client already sends it (whitelist_estimate_params). Deliberately not added to EventEstimateInstantFeesInput: Fees::EstimateInstant::PayInAdvanceService builds its Event without that attribute, so the asymmetry between the two estimate endpoints is real.

Filters / query params (1)

  • src/resources/events.yaml timestamp_from_started_at: documented that it cannot be combined with timestamp_from — evidence: Queries::EventsQueryFiltersContract fails the request with "cannot be used with timestamp_from_started_at" when both are present.

Verified correct, no change needed — GET /events documents exactly the five filters Api::V1::EventsController#index_filters permits. GET /billable_metrics correctly documents pagination only: BillableMetricsQuery supports recurring, aggregation_types and plan_id, but the controller passes neither filters: nor search_term, so none of them are reachable over the API. Paths, verbs and path params for both resources match config/routes/shared_api.rb.

[BREAKING-DOC] flags

  • BillableMetricObject.expression is now nullable. Consumers that assumed a non-null string must handle null. The code proves the API already returns null for every metric without an expression — this documents existing behaviour rather than changing it, but strict generated clients will see a type change.
  • BillableMetricObject.aggregation_type gained custom_agg. Consumers with exhaustive enum handling need a new branch. Note the blast radius is wider than slice 0: BillableMetricObject is embedded in the alert responses, so both changes also reach GET/PUT/DELETE /subscriptions/{external_id}/alerts*.

Contract compatibility impact

Decision: BLOCK — 9 blocking · 9 warning · 1 informational. Run in advisory mode against origin/main's bundle.

BLOCK is advisory and requires explicit human review; this agent never merges or approves.

The 9 blocking rows are one change (BillableMetricObject.expression becoming nullable) fanned out across the 9 operations that reuse the schema; likewise the 9 warnings are the single custom_agg enum addition. Both are code-proven and flagged [BREAKING-DOC] above. Full non-info findings:

Severity Operation Location Finding Required action
BLOCK DELETE /billable_metrics/{code} /responses/200/application/json/billable_metric/expression response value became nullable Preserve nullability or require explicit human approval.
BLOCK DELETE /subscriptions/{external_id}/alerts/{code} /responses/200/application/json/alert/billable_metric/expression response value became nullable Preserve nullability or require explicit human approval.
BLOCK GET /billable_metrics /responses/200/application/json/billable_metrics/items/expression response value became nullable Preserve nullability or require explicit human approval.
BLOCK GET /billable_metrics/{code} /responses/200/application/json/billable_metric/expression response value became nullable Preserve nullability or require explicit human approval.
BLOCK GET /subscriptions/{external_id}/alerts /responses/200/application/json/alerts/items/billable_metric/expression response value became nullable Preserve nullability or require explicit human approval.
BLOCK GET /subscriptions/{external_id}/alerts/{code} /responses/200/application/json/alert/billable_metric/expression response value became nullable Preserve nullability or require explicit human approval.
BLOCK POST /billable_metrics /responses/200/application/json/billable_metric/expression response value became nullable Preserve nullability or require explicit human approval.
BLOCK PUT /billable_metrics/{code} /responses/200/application/json/billable_metric/expression response value became nullable Preserve nullability or require explicit human approval.
BLOCK PUT /subscriptions/{external_id}/alerts/{code} /responses/200/application/json/alert/billable_metric/expression response value became nullable Preserve nullability or require explicit human approval.
WARN DELETE /billable_metrics/{code} /responses/200/application/json/billable_metric/aggregation_type response enum value "custom_agg" was added Check strict SDK enums and exhaustive consumers.
WARN DELETE /subscriptions/{external_id}/alerts/{code} /responses/200/application/json/alert/billable_metric/aggregation_type response enum value "custom_agg" was added Check strict SDK enums and exhaustive consumers.
WARN GET /billable_metrics /responses/200/application/json/billable_metrics/items/aggregation_type response enum value "custom_agg" was added Check strict SDK enums and exhaustive consumers.
WARN GET /billable_metrics/{code} /responses/200/application/json/billable_metric/aggregation_type response enum value "custom_agg" was added Check strict SDK enums and exhaustive consumers.
WARN GET /subscriptions/{external_id}/alerts /responses/200/application/json/alerts/items/billable_metric/aggregation_type response enum value "custom_agg" was added Check strict SDK enums and exhaustive consumers.
WARN GET /subscriptions/{external_id}/alerts/{code} /responses/200/application/json/alert/billable_metric/aggregation_type response enum value "custom_agg" was added Check strict SDK enums and exhaustive consumers.
WARN POST /billable_metrics /responses/200/application/json/billable_metric/aggregation_type response enum value "custom_agg" was added Check strict SDK enums and exhaustive consumers.
WARN PUT /billable_metrics/{code} /responses/200/application/json/billable_metric/aggregation_type response enum value "custom_agg" was added Check strict SDK enums and exhaustive consumers.
WARN PUT /subscriptions/{external_id}/alerts/{code} /responses/200/application/json/alert/billable_metric/aggregation_type response enum value "custom_agg" was added Check strict SDK enums and exhaustive consumers.

One informational finding (no action required): POST /events/estimate_fees — request property precise_total_amount_cents was added (additive).

Customer exposure: unknown. No authorized usage-evidence source was provided for this run, and the checker has no production telemetry. No customer identifiers appear in this PR.

SDK drift (spec is right — needs an sdk-clients-update run)

Resource Client(s) Divergence
billable_metrics Go AggregationType constants in billable_metric.go still carry recurring_count_agg, which BillableMetric::AGGREGATION_TYPES marks as a deleted type (slot 4), and omit both latest_agg and custom_agg.
billable_metrics Rust BillableMetricAggregationType (lago-types/src/models/billable_metric.rs) omits CustomAgg, so deserializing a custom-aggregator metric fails.
events Go EventEstimateFeesInput omits precise_total_amount_cents, which the endpoint honours and the Ruby client already sends.
billable_metrics, events Python No drift. Models are loosely typed (Optional[str] rather than enums) but nothing contradicts the spec; expression: Optional[str] corroborates the nullability fix above.
billable_metrics, events Ruby No drift for this slice. whitelist_params matches the permitted params on both resources.
— JavaScript No drift by construction: openapi/client.ts is generated at build time from https://swagger.getlago.com/openapi.yaml and is not committed, so there is no artifact to diff.

Needs human confirmation (not changed)

  • GET /events_enriched is absent from the spec. The route exists (config/routes/shared_api.rb:97 → Api::V1::EventsController#index_enriched) with a dedicated V1::EventEnrichedSerializer exposing enriched_at, value and decimal_value. It calls set_beta_header! and is gated behind ensure_organization_uses_clickhouse. Not added: whether a beta, org-gated endpoint belongs in the public contract is a product call.
  • external_contract_id is permitted on POST /events and POST /events/batch but undocumented. Events::CreateService folds it into external_subscription_id (external_subscription_id.presence || external_contract_id). The controller comment calls it "the v2 alias for external_subscription_id". Not added to a v1 spec without confirmation that it is a public, supported field.
  • external_subscription_id may be absent on events, but the spec treats it as mandatory and non-null. Event validates presence only on transaction_id and code, and the column is nullable, so POST /events accepts an event with no subscription reference and GET /events can return null. Not changed in either direction: relaxing EventInputObject.required would document unbillable events as valid, and making the response nullable is a consumer-breaking change resting on what looks more like API laxity than intent.
  • BillableMetricEvaluateExpressionInput over-declares required. The spec requires event plus event.code and event.properties, but BillableMetrics::EvaluateExpressionService enforces only expression (it does @event = event || {} and event["properties"]&.transform_values), and the controller uses params.permit, not params.require. Not relaxed: a call with no properties is accepted but useless for any expression that reads them, so the stricter spec arguably describes the intended contract.
  • counters fields are returned but undocumented. V1::BillableMetricSerializer merges active_subscriptions_count, draft_invoices_count and plans_count on create, update, destroy and index (but not show); all three are hardcoded to 0 and the controller marks the include DEPRECATED since 2024-11-22. Not documented — adding deprecated always-zero fields to the public contract needs a ruling.
  • Docs-guardian lead (Docs Guardian sweep — 2026-09-28 (slice 0: introduction, welcome, Lago Cloud, self-hosted, security) lago-doc#683): GET /security_logs and GET /security_logs/{log_id} are missing from the spec. Confirmed real — config/routes/shared_api.rb:5 declares resources :security_logs, param: :log_id, only: %i[index show], and guide/security/security-logs.mdx documents the feature, while /activity_logs and /api_logs are both specced. Not added here: like the resend_email family, this gap spans the spec, the api-reference pages and the SDK clients at once, so it wants a dedicated task rather than a sweep commit. Raising it once.
  • Possible typo left unfixed. The billable-metric expression description lists "+, -, \ and * operations"; \ is not an operator and is most likely /. Lago::ExpressionParser is a native extension outside lago-api, so the operator set could not be verified from the code. Left as-is rather than guessed.

Deferred to next run

  • Nothing deferred. 7 source files changed, well under the ~40-file cap.

Other docs-guardian leads triaged

Process feedback for the retro

  • Slack read access is unavailable. The $LAGO_SLACK_CLI token carries only chat:write, so conversations.history returns missing_scope, and the Slack MCP slack_get_channel_history fails identically. Slack thread feedback on previous guardian posts could not be read this run; GitHub was the only feedback channel. The last two docs-guardian runs reported the same gap, so this is now three consecutive runs — worth fixing the token scopes rather than re-reporting.
  • The compatibility checker counts operations, not changes. Two schema edits produced 18 non-info rows because BillableMetricObject is reused across billable-metric and alert responses. The BLOCK/WARN verdicts are right, but the counts overstate the size of the diff; a per-schema rollup alongside the per-operation table would make the handoff easier to read.
  • No human feedback to address this run. fix: OpenAPI guardian sweep 2026-09-21 (webhooks, webhook endpoints, activity logs, api logs, analytics) #584 (slice 7, opened 2026-09-21) has no comments, reviews or review comments, and the last five merged guardian PRs carry no unaddressed review feedback. Per the trust boundary, no PR comment needed write-access verification because there were none; the other open PRs on this repo are third-party contributions, not feedback on guardian branches. No LEARNINGS.md entry is proposed, since there is no identifiable human comment to back one.
  • fix: OpenAPI guardian sweep 2026-09-21 (webhooks, webhook endpoints, activity logs, api logs, analytics) #584 is stale: open 7 days with no review. Flagged in the Slack post.

🤖 Generated with Claude Code

lago-claude-ai-agent Bot and others added 3 commits September 28, 2026 08:06
- "evalutated" -> "evaluated" and "Accepted function are" -> "Accepted
  functions are" on the billable-metric expression descriptions
- "miliseconds" -> "milliseconds" on the event input timestamp

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
timestamp_from_started_at cannot be combined with timestamp_from:
Queries::EventsQueryFiltersContract fails the request with
"cannot be used with timestamp_from_started_at" when both are sent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BillableMetricObject:
- aggregation_type gains custom_agg. BillableMetric::AGGREGATION_TYPES
  maps custom_agg to 7 and V1::BillableMetricSerializer emits the enum
  name verbatim, so the API can return a value the spec rejected.
  custom_aggregator is never assigned from a permitted param, so the
  value is read-only over the API; the description says so.
- expression becomes nullable. db/structure.sql declares it
  "expression character varying" with no NOT NULL, and the serializer
  emits the column unconditionally.

EventObject:
- timestamp described as an ISO 8601 datetime, not a Unix timestamp in
  seconds: V1::EventSerializer emits model.timestamp.iso8601(3). The
  Unix-seconds form is the request format, already documented on
  EventInputObject.
- external_subscription_id no longer refers to external_customer_id,
  which Api::V1::EventsController#create_params does not permit.

EventEstimateFeesInput:
- document precise_total_amount_cents. estimate_fees runs on
  create_params, which permits it, and
  Fees::EstimatePayInAdvanceService reads
  event_params[:precise_total_amount_cents] (defaulting to 0) when
  building the estimated event.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

0 participants