Files
zeekayandHanzo Dev 5ac678fd84 no hogs: the person services, the mock's customer, and one enum member
PersonHog was the last of it, and it was not only a name. The package is
personinsights and the types in it are already Person* -- the shipped
descriptor under personinsights_client/proto/generated says PersonService
and the tests say TestFakePersonClient -- but the .proto still declared
PersonHogService. A client dialling one service name and a server built
from a .proto declaring another is not a naming problem, it is a wire
that does not connect; the only reason nobody hit it is that none of
these services runs here yet. Five service declarations now say what the
generated client has said all along.

StreamHog was a second fictional company, invented so a Stripe mock had
someone to bill. This fork already has Hedgebox, in five hundred
fixtures, so the mock bills Hedgebox and there is one made-up company
again instead of two.

REVIEW_HOG was an enum member in three enums that had to agree, whose
value has been `review` since the app was renamed. Only the member said
otherwise.

Every rule is in bin/debrand and every spelling is refused there, so a
fresh upstream import lands on these names and none of them can come
back. Two are named as outstanding rather than refused, with the reason
each is still sayable: hogdesk survives inside applied migrations'
choices lists, where Django compares it in Python and never writes it --
the member is Desk = "desk" and all three tables that store it hold no
rows; sherlockhog keys a service in another repository that still calls
itself that, and renaming our half would classify its traffic as a
product that does not exist.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 13:57:50 -07:00

4.2 KiB

Proto conventions and worked example

Message conventions

  • int64 for IDs, timestamps (Unix millis), versions
  • bytes for JSON data (not string) — properties, properties_last_updated_at, properties_last_operation, default_columns
  • optional for nullable fields
  • Stay at v1 for additive changes; bump to v2 only for breaking changes
  • Never reuse field numbers after deletion
  • Request messages: <RpcName>Request
  • Response messages: <RpcName>Response (or a shared response type like PersonsResponse if appropriate)
  • Include ReadOptions read_options field on read requests that need consistency control

ReadOptions and consistency

// Already defined in common.proto — import it, don't redefine
import "personinsights/types/v1/common.proto";

message YourReadRequest {
  int64 team_id = 1;
  // ... other fields ...
  ReadOptions read_options = N;  // last field
}

ReadOptions contains a ConsistencyLevel enum:

  • EVENTUAL (default) — queries the replica pool
  • STRONG — queries the primary pool (only meaningful for PersonData reads)

For NonPersonData, the replica handles consistency internally — the router always sends to replica regardless of consistency level.

File organization

Proto file Purpose
proto/personinsights/types/v1/person.proto Person, DistinctId messages and all person-related request/response types
proto/personinsights/types/v1/group.proto Group, GroupTypeMapping messages and all group-related request/response types
proto/personinsights/types/v1/cohort.proto CohortMembership messages and cohort-related request/response types
proto/personinsights/types/v1/feature_flag.proto HashKeyOverride messages and feature flag request/response types
proto/personinsights/types/v1/common.proto ReadOptions, ConsistencyLevel, TeamDistinctId — shared across domains
proto/personinsights/service/v1/service.proto PersonService — public API (what clients call)
proto/personinsights/replica/v1/replica.proto PersonReplica — internal API (what the router calls)
proto/personinsights/leader/v1/leader.proto PersonLeader — internal write API for person data

Worked example: adding GetPersonCount

1. Add messages to types/v1/person.proto

message GetPersonCountRequest {
  int64 team_id = 1;
  ReadOptions read_options = 2;
}

message GetPersonCountResponse {
  int64 count = 1;
}

2. Add RPC to service/v1/service.proto

service PersonService {
  // ... existing RPCs ...
  rpc GetPersonCount(personinsights.types.v1.GetPersonCountRequest) returns (personinsights.types.v1.GetPersonCountResponse);
}

3. Add RPC to replica/v1/replica.proto

service PersonReplica {
  // ... existing RPCs ...
  rpc GetPersonCount(personinsights.types.v1.GetPersonCountRequest) returns (personinsights.types.v1.GetPersonCountResponse);
}

The RPC signature must be identical in both service and replica protos.

4. Routing decision

This is a PersonData read → routed to replica for EVENTUAL, leader for STRONG. In the router, use:

let route = route_request(
    DataCategory::PersonData,
    OperationType::Read,
    get_consistency(&request.read_options),
)?;

5. Python client wrapper (in client.py)

def get_person_count(self, request: GetPersonCountRequest) -> GetPersonCountResponse:
    return self._stub.GetPersonCount(request, timeout=self._timeout)

6. Fake client method (in fake_client.py)

def get_person_count(self, request: Any) -> Any:
    team_id = request.team_id
    count = sum(1 for (tid, _) in self._persons_by_id if tid == team_id)
    call = _Call("get_person_count", request)
    resp = person_pb2.GetPersonCountResponse(count=count)  # or however the response is built
    call.response = resp
    self.calls.append(call)
    return resp