|
| 1 | +<Info> |
| 2 | +**Status**: Active |
| 3 | +**Created**: October 2025 |
| 4 | +**Last Updated**: November 2025 |
| 5 | +</Info> |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +Increase the scope and functionality of Polar events to allow for aggregating multiple events and tying them together, ultimately to be able to act on the success or failure of a product flow in a meter. |
| 10 | + |
| 11 | +## Goals |
| 12 | + |
| 13 | +* Make it possible for Polar to give more actionable insights based on how customers are using a product. |
| 14 | +* Allow someone to experiment and determine costs and ideal pricing for different parts of a product. |
| 15 | + |
| 16 | +## Events |
| 17 | + |
| 18 | +### Data model |
| 19 | + |
| 20 | +| id | name | external_id`*` | parent_id`*` | ...metadata | |
| 21 | +| ------------------ | ------------- | ------------------ | ---------------- | ------------- | |
| 22 | +| \<Polar uuid-1> | Start event | $user\ specified$ | `null` | {... anything }| |
| 23 | +| \<Polar uuid-2> | Nested event | $user\ specified$ | \<Polar uuid-2> | {... anything }| |
| 24 | +| \<Polar uuid-3> | Nested event | $user\ specified$ | \<Polar uuid-3> | {... anything }| |
| 25 | + |
| 26 | +`*`: New field. |
| 27 | + |
| 28 | +By adding an `external_id` to the `events` we gain an idempotency key on ingested events, making it unproblematic to re-ingest the same events multiple times. We can then leverage the `external_id` as the identifier to specify both the id on an event as well as the parent id of an event. |
| 29 | + |
| 30 | +Internally we don't want to store the relationship between two events via an user-specified ID, but we can validate and translate the specified `parent_id` on the ingestion of an event thus ensuring the relationship is stored by Polar IDs. |
| 31 | + |
| 32 | +### Flowchart |
| 33 | + |
| 34 | +```mermaid |
| 35 | +sequenceDiagram |
| 36 | + participant Parrot |
| 37 | + participant Polar SDK |
| 38 | + participant Polar API |
| 39 | + % participant Events |
| 40 | +
|
| 41 | + Parrot->>Polar SDK: withSpan(externalId: 'parrot-internal-id') |
| 42 | + Polar SDK->>Polar API: POST /events/ingest |
| 43 | +
|
| 44 | + Polar SDK->>Polar SDK: Mark instance as parentEventId = parrot-internal-id |
| 45 | + Parrot->>Polar SDK: sendEvent(name, metadata) |
| 46 | + Polar SDK->>Polar API: POST /events/ingest {name, metadata, parentEventId = Parrot internal id} |
| 47 | + note over Polar SDK, Polar API: Lookup externalId to get Polar ID and set Polar ID on parent_id |
| 48 | +``` |
| 49 | + |
| 50 | +## Sequences |
| 51 | + |
| 52 | +A subset (or a full) hierarchy of events can be thought of as a sequence of events. |
| 53 | + |
| 54 | +A sequence groups related events via parent-child hierarchies. Each root event that matches a creation criteria creates its own sequence. Descendant events (via `parent_id`) are automatically added to the same sequence. Cost and revenue are aggregated on each sequence. |
| 55 | + |
| 56 | +Sequences that have the same definition (creation criteria) can then be aggregated or compared to each other to be able to answer questions such as: |
| 57 | + |
| 58 | +* How does this sequence compare to the average sequence. |
| 59 | +* How does this customer compare to the average customer in terms of |
| 60 | + * Usage |
| 61 | + * Cost |
| 62 | + |
| 63 | +### Data model |
| 64 | + |
| 65 | +```mermaid |
| 66 | +erDiagram |
| 67 | + direction LR |
| 68 | + EventSequenceDefinition ||--o{ EventSequence : "creates instances" |
| 69 | + EventSequence ||--o{ EventToSequence : "contains" |
| 70 | + Event ||--o{ EventToSequence : "belongs to" |
| 71 | + Event ||--o| Event : "parent_id" |
| 72 | + Organization ||--o{ EventSequenceDefinition : "owns" |
| 73 | +
|
| 74 | + EventSequenceDefinition { |
| 75 | + uuid id PK |
| 76 | + string name |
| 77 | + Filter creation_criteria |
| 78 | + jsonb config |
| 79 | + uuid organization_id FK |
| 80 | + } |
| 81 | +
|
| 82 | + EventSequence { |
| 83 | + uuid id PK |
| 84 | + uuid event_sequence_definition_id FK |
| 85 | + jsonb aggregated_data |
| 86 | + string label |
| 87 | + timestamp first_event_at |
| 88 | + timestamp last_event_at |
| 89 | + } |
| 90 | +
|
| 91 | + EventToSequence { |
| 92 | + uuid event_id PK,FK |
| 93 | + uuid event_sequence_id PK,FK |
| 94 | + boolean is_root |
| 95 | + } |
| 96 | +
|
| 97 | + Event { |
| 98 | + uuid id PK |
| 99 | + string name |
| 100 | + uuid parent_id FK |
| 101 | + uuid customer_id FK |
| 102 | + jsonb user_metadata |
| 103 | + timestamp timestamp |
| 104 | + } |
| 105 | +``` |
| 106 | + |
| 107 | + |
| 108 | +#### Root vs Descendant Events |
| 109 | + |
| 110 | +- **Root events**: Events that match the creation_criteria in EventSequenceDefinition and create a new sequence |
| 111 | +- **Descendant events**: Child/grandchild events found via `parent_id` traversal |
| 112 | +- Both stored in `EventToSequence`, distinguished by `is_root` flag to easily allow querying the root events for a listing. |
| 113 | + |
| 114 | +#### Sequence Instances |
| 115 | + |
| 116 | +Each root event creates its **own** EventSequence instance: |
| 117 | + |
| 118 | +``` |
| 119 | +Event A: support_request.created (+ 4 descendants) |
| 120 | + → EventSequence X |
| 121 | +
|
| 122 | +Event B: support_request.created (+ 2 descendants) |
| 123 | + → EventSequence Y |
| 124 | +
|
| 125 | +Event C: support_request.created (+ 1 descendant) |
| 126 | + → EventSequence Z |
| 127 | +
|
| 128 | +Three separate, unrelated support request sequences |
| 129 | +``` |
| 130 | + |
| 131 | +<OpenQuestion> |
| 132 | +The proposal is to let a sequence only have a single outcome defined, and if a hierarchy of events can have multiple outcomes we would prefer the user to set up multiple sequences with the same creation criteria. |
| 133 | + |
| 134 | +This simplifies the creation and understanding of what a single sequence (or sequence definition) is. |
| 135 | + |
| 136 | +It does not solve the comparison between multiple sequences with different definitions. |
| 137 | +</OpenQuestion> |
0 commit comments