-
-
Notifications
You must be signed in to change notification settings - Fork 2
feat(billing): Add seat activity and seats service protos #411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f4f266a
250b06c
742e00f
86407f7
092a501
7cdea9f
6af66b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.common.v1; | ||
|
|
||
| // Lifecycle status of a billing seat. Shared by both the seat_activity and | ||
| // seats services. | ||
| enum SeatStatus { | ||
| SEAT_STATUS_UNSPECIFIED = 0; | ||
| // Seat is active and counting toward the billing period. | ||
| SEAT_STATUS_ACTIVE = 1; | ||
| // Seat is disabled; it still counts toward the period total but new | ||
| // activity (e.g. check-ins) is dropped. | ||
| SEAT_STATUS_DISABLED = 2; | ||
| // Seat was created but exceeds reserved+PAYG capacity. Treated as | ||
| // DISABLED by external consumers (the distinction is internal only). | ||
| SEAT_STATUS_OVER_QUOTA = 3; | ||
| // Seat does not exist (removed or never created). | ||
| SEAT_STATUS_DNE = 4; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seat_activity.v1; | ||
|
|
||
| // Marks a contract's seat tracker as CLOSED, preventing further seat actions. | ||
| // Called during rollover before writing seats to the new contract. | ||
| // Silently succeeds if no tracker exists for the contract. | ||
| message CloseContractRequest { | ||
| uint64 contract_id = 1; | ||
| } | ||
|
|
||
| message CloseContractResponse {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seat_activity.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/seat_category.proto"; | ||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
|
|
||
| // Returns each seat's earliest-ever recorded action timestamp across the | ||
| // entire organization (not scoped to a single contract). Used during contract | ||
| // rollover to sort seats by seniority when capacity is tight. | ||
| message GetOriginalSeatDatesRequest { | ||
| uint64 organization_id = 1; | ||
| sentry_protos.billing.v1.SeatCategory seat_category = 2; | ||
| repeated string external_product_identifiers = 3; | ||
| } | ||
|
|
||
| message GetOriginalSeatDatesResponse { | ||
| repeated OriginalSeatDate original_seat_dates = 1; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seat_activity.v1; | ||
|
|
||
| import "google/protobuf/timestamp.proto"; | ||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
|
|
||
| // Returns every seat action recorded for a contract, grouped by product | ||
| // identifier and ordered chronologically. This is the raw audit log of all | ||
| // state transitions during a billing period. | ||
| message GetSeatActivityHistoryRequest { | ||
| uint64 contract_id = 1; | ||
| } | ||
|
|
||
| message GetSeatActivityHistoryResponse { | ||
| repeated ExternalProductSeatActivity seat_activities = 1; | ||
| google.protobuf.Timestamp read_ts = 2; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seat_activity.v1; | ||
|
|
||
| // Checks whether a contract has a seat tracking record and whether it is | ||
| // ACTIVE. Used by the seats service to decide whether to bootstrap a new | ||
| // tracker or trigger a rollover from a previous contract. | ||
| message GetSeatContractStateRequest { | ||
| uint64 contract_id = 1; | ||
| } | ||
|
|
||
| message GetSeatContractStateResponse { | ||
| bool exists = 1; | ||
| bool is_active = 2; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seat_activity.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
|
|
||
| // Returns the current status of a single seat by replaying its action history | ||
| // on the given contract (last action wins). Returns DNE if no actions exist. | ||
| message GetSeatStateRequest { | ||
| uint64 contract_id = 1; | ||
| SeatObject seat_object = 2; | ||
| } | ||
|
|
||
| message GetSeatStateResponse { | ||
| SeatState seat_state = 1; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seat_activity.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
|
|
||
| // The single write path for all seat mutations. Appends action records in bulk | ||
| // under a transaction. Does NOT check budgets or eligibility — that is the | ||
| // caller's responsibility. | ||
| // | ||
| // Rejects duplicate identifiers within a single request. Requires an ACTIVE | ||
| // contract tracker unless create_new_contract_state is set. | ||
| message PerformSeatActionsRequest { | ||
| uint64 contract_id = 1; | ||
| repeated SeatActionItem seat_actions = 2; | ||
| // When true, creates a new ACTIVE contract tracker for this contract. | ||
| // Fails if one already exists (used for first-time setup and rollover). | ||
| bool create_new_contract_state = 3; | ||
| } | ||
|
|
||
| message PerformSeatActionsResponse { | ||
| repeated SeatState current_seat_states = 1; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seat_activity.v1; | ||
|
|
||
| import "google/protobuf/timestamp.proto"; | ||
| import "sentry_protos/billing/v1/common/v1/seat_status.proto"; | ||
| import "sentry_protos/billing/v1/seat_category.proto"; | ||
|
|
||
| // Identifies a billable seat (e.g. a monitor, uptime detector, or seer | ||
| // contributor) within an organization and project. | ||
| message SeatObject { | ||
| uint64 organization_id = 1; | ||
| uint64 project_id = 2; | ||
| sentry_protos.billing.v1.SeatCategory seat_category = 3; | ||
| // Human-readable label for the seat (e.g. monitor name). | ||
| string external_product_display_name = 4; | ||
| // Unique identifier for the seat within its category and project | ||
| // (e.g. monitor slug). Used as the key for all state lookups. | ||
| string external_product_identifier = 5; | ||
| } | ||
|
|
||
| // Reconstructed point-in-time state for a single seat, derived by replaying | ||
| // its action history (last action wins). | ||
| message SeatState { | ||
| string external_product_identifier = 1; | ||
| sentry_protos.billing.v1.common.v1.SeatStatus current_status = 2; | ||
| // Timestamp of the first-ever action for this seat on the contract. | ||
| optional google.protobuf.Timestamp created_ts = 3; | ||
| // Timestamp of the most recent action for this seat on the contract. | ||
| optional google.protobuf.Timestamp last_action_ts = 4; | ||
| // When the state was read (server clock). Useful for cache staleness checks. | ||
| google.protobuf.Timestamp read_ts = 5; | ||
| } | ||
|
|
||
| // A single immutable record of a seat state transition, stored as an | ||
| // append-only audit log. | ||
| message SeatActionRecord { | ||
| uint64 contract_id = 1; | ||
| uint64 organization_id = 2; | ||
| uint64 project_id = 3; | ||
| google.protobuf.Timestamp timestamp = 4; | ||
| sentry_protos.billing.v1.common.v1.SeatStatus status = 5; | ||
| uint32 seat_category = 6; | ||
| string external_product_identifier = 7; | ||
| string external_product_display_name = 8; | ||
| } | ||
|
|
||
| // All action records for a single external product identifier on a contract, | ||
| // ordered chronologically. | ||
| message ExternalProductSeatActivity { | ||
| string external_product_identifier = 1; | ||
| repeated SeatActionRecord seat_actions = 2; | ||
| } | ||
|
|
||
| // Input item for PerformSeatActions: pairs a seat with its desired new state. | ||
| message SeatActionItem { | ||
| SeatObject seat_object = 1; | ||
|
Comment on lines
+54
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixRefactor Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| SeatState seat_state = 2; | ||
| } | ||
|
|
||
| // The earliest-ever recorded action timestamp for a seat across all contracts | ||
| // in an organization. Used to sort seats by seniority during rollover. | ||
| message OriginalSeatDate { | ||
| string external_product_identifier = 1; | ||
| google.protobuf.Timestamp created_ts = 2; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
| import "sentry_protos/billing/v1/services/seats/v1/seats.proto"; | ||
|
|
||
| // Assigns a single billing seat for the current contract period. Internally | ||
| // runs check_assign_seats to determine capacity: ACCEPTED seats become ACTIVE, | ||
| // RATE_LIMITED seats become OVER_QUOTA, INVALID seats are rejected. | ||
| // Lazily initializes the contract's seat tracker on first write. | ||
| message AssignSeatRequest { | ||
| sentry_protos.billing.v1.services.seat_activity.v1.SeatObject seat_object = 1; | ||
| } | ||
|
|
||
| message AssignSeatResponse { | ||
| SeatAssignmentOutcome outcome = 1; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
| import "sentry_protos/billing/v1/services/seats/v1/seats.proto"; | ||
|
|
||
| // Dry-run check: can these seats be assigned? Validates the product is enabled | ||
| // on the plan, filters out already-existing seats, then simulates usage pricing | ||
| // to determine if reserved or PAYG budgets have capacity. | ||
| // All seat_objects must share the same seat category and organization. | ||
| message CheckAssignSeatsRequest { | ||
| repeated sentry_protos.billing.v1.services.seat_activity.v1.SeatObject seat_objects = 1; | ||
| // When omitted, resolves to the org's current contract. | ||
| optional uint64 contract_id = 2; | ||
| } | ||
|
|
||
| message CheckAssignSeatsResponse { | ||
| SeatAssignmentOutcome outcome = 1; | ||
| string reason = 2; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
|
|
||
| // Sets a seat to DISABLED status. The seat still counts toward the period | ||
| // total but stops accepting new activity (e.g. check-ins are dropped). | ||
| // Only allowed for seat categories whose policy supports disabling. | ||
| message DisableSeatRequest { | ||
| sentry_protos.billing.v1.services.seat_activity.v1.SeatObject seat_object = 1; | ||
| // When omitted, resolves to the org's current contract. | ||
| optional uint64 contract_id = 2; | ||
| } | ||
|
|
||
| message DisableSeatResponse {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/usage/v1/endpoint_usage.proto"; | ||
|
|
||
| // Response-only proto; the request reuses GetUsageRequest from the usage service. | ||
| // Returns per-day billable and active seat counts for the requested period. | ||
| message GetDailySeatUsageResponse { | ||
| repeated sentry_protos.billing.v1.services.usage.v1.DailySeatUsage seat_days = 1; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/seats/v1/seats.proto"; | ||
|
|
||
| // Response-only proto; the request reuses GetUsageByProjectRequest from the | ||
| // usage service. Same as GetDailySeatUsageResponse but broken out per project. | ||
| message GetDailySeatUsageByProjectResponse { | ||
| repeated ProjectSeatUsage projects = 1; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/common/v1/seat_status.proto"; | ||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
|
|
||
| // High-throughput read path called by the monitor consumer on every check-in. | ||
| // Looks up the seat's current state and maps it to an external-facing status. | ||
| // OVER_QUOTA is mapped to DISABLED — external consumers do not distinguish | ||
| // between the two. | ||
| message GetSeatStatusForExternalProductRequest { | ||
| sentry_protos.billing.v1.services.seat_activity.v1.SeatObject seat_object = 1; | ||
| } | ||
|
|
||
| message GetSeatStatusForExternalProductResponse { | ||
| sentry_protos.billing.v1.common.v1.SeatStatus status = 1; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
|
|
||
| // Permanently removes a seat (status -> DNE), freeing the slot for | ||
| // reallocation. Only allowed for seat categories whose policy supports removal. | ||
| message RemoveSeatRequest { | ||
| sentry_protos.billing.v1.services.seat_activity.v1.SeatObject seat_object = 1; | ||
| // When omitted, resolves to the org's current contract. | ||
| optional uint64 contract_id = 2; | ||
| } | ||
|
|
||
| message RemoveSeatResponse {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| // Recreates carry-over seats when a contract rolls over. Closes the old | ||
| // contract's tracker, collects seats that ended ACTIVE or OVER_QUOTA, orders | ||
| // them by seniority, and splits against the new contract's capacity (fits -> | ||
| // ACTIVE, overflow -> OVER_QUOTA). No-ops if the new contract already has | ||
| // seat state (idempotent on retry). | ||
| message RolloverSeatsToNewContractRequest { | ||
| uint64 prev_contract_id = 1; | ||
| uint64 new_contract_id = 2; | ||
| } | ||
|
|
||
| message RolloverSeatsToNewContractResponse {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/seat_activity/v1/seat_activity.proto"; | ||
|
|
||
| // Atomically renames a seat's identifier (e.g. when a monitor slug changes). | ||
| // Implemented as two actions in one batch: old identifier -> DNE, new | ||
| // identifier -> previous status. Only allowed for seat categories whose | ||
| // policy supports renaming. | ||
| message UpdateSeatIdentifierRequest { | ||
| sentry_protos.billing.v1.services.seat_activity.v1.SeatObject seat_object = 1; | ||
| string new_identifier = 2; | ||
| // When omitted, resolves to the org's current contract. | ||
| optional uint64 contract_id = 3; | ||
| } | ||
|
|
||
| message UpdateSeatIdentifierResponse {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package sentry_protos.billing.v1.services.seats.v1; | ||
|
|
||
| import "sentry_protos/billing/v1/services/usage/v1/endpoint_usage.proto"; | ||
|
|
||
| // Result of a seat assignment or check_assign_seats call. | ||
| enum SeatAssignmentOutcome { | ||
| SEAT_ASSIGNMENT_OUTCOME_UNSPECIFIED = 0; | ||
| // Seat fits within reserved or PAYG capacity. | ||
| SEAT_ASSIGNMENT_OUTCOME_ACCEPTED = 1; | ||
| // No budget remaining; seat is created as OVER_QUOTA on assign. | ||
| SEAT_ASSIGNMENT_OUTCOME_RATE_LIMITED = 2; | ||
| // Product not enabled on plan or request is structurally invalid. | ||
| SEAT_ASSIGNMENT_OUTCOME_INVALID = 3; | ||
| } | ||
|
|
||
| // Per-project seat usage over a date range, used by get_daily_seat_usage_by_project. | ||
| message ProjectSeatUsage { | ||
| uint64 project_id = 1; | ||
| repeated sentry_protos.billing.v1.services.usage.v1.DailySeatUsage seat_days = 2; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seat category uses raw integer type
Medium Severity
SeatActionRecord.seat_categoryis auint32while every other seat-category field in this change uses theSeatCategoryenum, includingSeatObjectin the same file. Callers get a raw number instead of a typed value, and changing the field later is a breaking generated-code change.Reviewed by Cursor Bugbot for commit 092a501. Configure here.