fix: OpenAPI guardian sweep 2026-09-28 (billable metrics, events) - #586
Open
lago-claude-ai-agent[bot] wants to merge 3 commits into
Open
lago-claude-ai-agent[bot] wants to merge 3 commits into
lago-claude-ai-agent[bot] wants to merge 3 commits into
Conversation
- "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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 buildandnpm run testpass on this branch (0 errors; the 23array-params-pluralwarnings are pre-existing onmainand untouched here).Slice picked as ISO week 40 % 8 = 0 (weekly cadence for this run).
Fixed in this PR
Field-level evidence
Typos (7)
src/schemas/BillableMetricObject.yaml,src/schemas/BillableMetricBaseInput.yaml,src/schemas/BillableMetricEvaluateExpressionInput.yaml—expressiondescription: "evalutated" → "evaluated".src/schemas/BillableMetricBaseInput.yaml,src/schemas/BillableMetricEvaluateExpressionInput.yaml— "Accepted function are" → "Accepted functions are".src/schemas/EventInputObject.yamltimestamp: "miliseconds" → "milliseconds".src/schemas/EventObject.yamlprecise_total_amount_cents: "This filed is used by" → "This field is used by".Definitions (2)
src/schemas/EventObject.yamltimestamp: described as "the Unix timestamp in seconds" → described as an ISO 8601 datetime with millisecond precision — evidence:V1::EventSerializeremitsmodel.timestamp.iso8601(3)(app/serializers/v1/event_serializer.rb). The Unix-seconds form is the request format and is already documented correctly onEventInputObject.src/schemas/EventObject.yamlexternal_subscription_id: dropped the clause "mandatory ... when theexternal_customer_idis not provided" — evidence:Api::V1::EventsController#create_paramsdoes not permitexternal_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.yamlaggregation_type: enum gainscustom_agg— evidence:BillableMetric::AGGREGATION_TYPESmapscustom_agg: 7(app/models/billable_metric.rb:29-38),V1::BillableMetricSerializeremits the enum name verbatim, and theexports_billable_metricsview indb/structure.sqlmaps7 → 'custom_agg'. The API could return a value the spec rejected. Left out ofBillableMetricBaseInputdeliberately:custom_aggrequirescustom_aggregator(validates :custom_aggregator, presence: true, if: :custom_agg?), which no permitted param ever assigns, so acustom_aggcreate/update always 422s. The description now states it is response-only.src/schemas/BillableMetricObject.yamlexpression:string→[string, "null"]— evidence:db/structure.sqldeclaresexpression character varyingwith noNOT NULLand 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: addedprecise_total_amount_cents— evidence:estimate_feesruns oncreate_params, which permits it, andFees::EstimatePayInAdvanceServicereadsevent_params[:precise_total_amount_cents] || 0when building the estimated event. The Ruby client already sends it (whitelist_estimate_params). Deliberately not added toEventEstimateInstantFeesInput:Fees::EstimateInstant::PayInAdvanceServicebuilds itsEventwithout that attribute, so the asymmetry between the two estimate endpoints is real.Filters / query params (1)
src/resources/events.yamltimestamp_from_started_at: documented that it cannot be combined withtimestamp_from— evidence:Queries::EventsQueryFiltersContractfails the request with"cannot be used with timestamp_from_started_at"when both are present.Verified correct, no change needed —
GET /eventsdocuments exactly the five filtersApi::V1::EventsController#index_filterspermits.GET /billable_metricscorrectly documents pagination only:BillableMetricsQuerysupportsrecurring,aggregation_typesandplan_id, but the controller passes neitherfilters:norsearch_term, so none of them are reachable over the API. Paths, verbs and path params for both resources matchconfig/routes/shared_api.rb.[BREAKING-DOC] flags
BillableMetricObject.expressionis now nullable. Consumers that assumed a non-null string must handlenull. The code proves the API already returnsnullfor every metric without an expression — this documents existing behaviour rather than changing it, but strict generated clients will see a type change.BillableMetricObject.aggregation_typegainedcustom_agg. Consumers with exhaustive enum handling need a new branch. Note the blast radius is wider than slice 0:BillableMetricObjectis embedded in the alert responses, so both changes also reachGET/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.BLOCKis advisory and requires explicit human review; this agent never merges or approves.The 9 blocking rows are one change (
BillableMetricObject.expressionbecoming nullable) fanned out across the 9 operations that reuse the schema; likewise the 9 warnings are the singlecustom_aggenum addition. Both are code-proven and flagged[BREAKING-DOC]above. Full non-info findings:One informational finding (no action required):
POST /events/estimate_fees— request propertyprecise_total_amount_centswas 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-updaterun)AggregationTypeconstants inbillable_metric.gostill carryrecurring_count_agg, whichBillableMetric::AGGREGATION_TYPESmarks as a deleted type (slot 4), and omit bothlatest_aggandcustom_agg.BillableMetricAggregationType(lago-types/src/models/billable_metric.rs) omitsCustomAgg, so deserializing a custom-aggregator metric fails.EventEstimateFeesInputomitsprecise_total_amount_cents, which the endpoint honours and the Ruby client already sends.Optional[str]rather than enums) but nothing contradicts the spec;expression: Optional[str]corroborates the nullability fix above.whitelist_paramsmatches the permitted params on both resources.openapi/client.tsis generated at build time fromhttps://swagger.getlago.com/openapi.yamland is not committed, so there is no artifact to diff.Needs human confirmation (not changed)
GET /events_enrichedis absent from the spec. The route exists (config/routes/shared_api.rb:97→Api::V1::EventsController#index_enriched) with a dedicatedV1::EventEnrichedSerializerexposingenriched_at,valueanddecimal_value. It callsset_beta_header!and is gated behindensure_organization_uses_clickhouse. Not added: whether a beta, org-gated endpoint belongs in the public contract is a product call.external_contract_idis permitted onPOST /eventsandPOST /events/batchbut undocumented.Events::CreateServicefolds it intoexternal_subscription_id(external_subscription_id.presence || external_contract_id). The controller comment calls it "the v2 alias forexternal_subscription_id". Not added to a v1 spec without confirmation that it is a public, supported field.external_subscription_idmay be absent on events, but the spec treats it as mandatory and non-null.Eventvalidates presence only ontransaction_idandcode, and the column is nullable, soPOST /eventsaccepts an event with no subscription reference andGET /eventscan returnnull. Not changed in either direction: relaxingEventInputObject.requiredwould 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.BillableMetricEvaluateExpressionInputover-declaresrequired. The spec requireseventplusevent.codeandevent.properties, butBillableMetrics::EvaluateExpressionServiceenforces onlyexpression(it does@event = event || {}andevent["properties"]&.transform_values), and the controller usesparams.permit, notparams.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.countersfields are returned but undocumented.V1::BillableMetricSerializermergesactive_subscriptions_count,draft_invoices_countandplans_counton create, update, destroy and index (but not show); all three are hardcoded to0and the controller marks the includeDEPRECATED since 2024-11-22. Not documented — adding deprecated always-zero fields to the public contract needs a ruling.GET /security_logsandGET /security_logs/{log_id}are missing from the spec. Confirmed real —config/routes/shared_api.rb:5declaresresources :security_logs, param: :log_id, only: %i[index show], andguide/security/security-logs.mdxdocuments the feature, while/activity_logsand/api_logsare both specced. Not added here: like theresend_emailfamily, 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.+,-,\and*operations";\is not an operator and is most likely/.Lago::ExpressionParseris a native extension outsidelago-api, so the operator set could not be verified from the code. Left as-is rather than guessed.Deferred to next run
Other docs-guardian leads triaged
invoice.add_on_addedwebhook. Already resolved: fix(webhook): deprecate the stale invoice.add_on_added webhook #577 deprecated it on 2026-09-08. No action.Process feedback for the retro
$LAGO_SLACK_CLItoken carries onlychat:write, soconversations.historyreturnsmissing_scope, and the Slack MCPslack_get_channel_historyfails 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.BillableMetricObjectis reused across billable-metric and alert responses. TheBLOCK/WARNverdicts 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.LEARNINGS.mdentry is proposed, since there is no identifiable human comment to back one.🤖 Generated with Claude Code