Skip to main content

hanzo_client/apis/
event_api.rs

1/*
2 * Hanzo Cloud API
3 *
4 * The Hanzo Cloud API as a customer calls it: every operation under /v1/ except the operator's admin product, relay routes, legacy spellings and capabilities still reached by flag. Tagged by product: the first path segment after /v1/.
5 *
6 * The version of the OpenAPI document: v1
7 * 
8 * Generated by: https://openapi-generator.tech
9 */
10
11
12use reqwest;
13use serde::{Deserialize, Serialize, de::Error as _};
14use crate::{apis::ResponseContent, models};
15use super::{Error, configuration, ContentType};
16
17
18/// struct for typed errors of method [`get_event_errors`]
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum GetEventErrorsError {
22    UnknownValue(serde_json::Value),
23}
24
25/// struct for typed errors of method [`get_event_health`]
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(untagged)]
28pub enum GetEventHealthError {
29    Status503(models::HealthReport),
30    UnknownValue(serde_json::Value),
31}
32
33/// struct for typed errors of method [`get_event_insights_events`]
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(untagged)]
36pub enum GetEventInsightsEventsError {
37    UnknownValue(serde_json::Value),
38}
39
40/// struct for typed errors of method [`get_event_insights_health`]
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(untagged)]
43pub enum GetEventInsightsHealthError {
44    UnknownValue(serde_json::Value),
45}
46
47/// struct for typed errors of method [`get_event_overview`]
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(untagged)]
50pub enum GetEventOverviewError {
51    UnknownValue(serde_json::Value),
52}
53
54/// struct for typed errors of method [`get_event_tag_period_js`]
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(untagged)]
57pub enum GetEventTagPeriodJsError {
58    UnknownValue(serde_json::Value),
59}
60
61/// struct for typed errors of method [`get_event_timeseries`]
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(untagged)]
64pub enum GetEventTimeseriesError {
65    UnknownValue(serde_json::Value),
66}
67
68/// struct for typed errors of method [`get_event_top`]
69#[derive(Debug, Clone, Serialize, Deserialize)]
70#[serde(untagged)]
71pub enum GetEventTopError {
72    UnknownValue(serde_json::Value),
73}
74
75/// struct for typed errors of method [`post_event`]
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(untagged)]
78pub enum PostEventError {
79    UnknownValue(serde_json::Value),
80}
81
82/// struct for typed errors of method [`post_event_by_project_envelope`]
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(untagged)]
85pub enum PostEventByProjectEnvelopeError {
86    UnknownValue(serde_json::Value),
87}
88
89/// struct for typed errors of method [`post_event_by_project_store`]
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(untagged)]
92pub enum PostEventByProjectStoreError {
93    UnknownValue(serde_json::Value),
94}
95
96/// struct for typed errors of method [`post_event_replay`]
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(untagged)]
99pub enum PostEventReplayError {
100    UnknownValue(serde_json::Value),
101}
102
103
104/// Errors returns the caller org's most recently captured errors, newest first. The error-tracking read view over event.error — the plane table the write core's error facts land in (errors are DELIBERATELY not on event.event) — each with its captured exception surfaced from the attributes map as a first-class field.  The org is the validated principal's — never a parameter — and this read requires a real bearer, NEVER the write-only publishable key: pk- can attribute a write and can read nothing. 403 without a validated bearer, 503 when the warehouse is unreachable.
105pub async fn get_event_errors(configuration: &configuration::Configuration, limit: Option<i32>) -> Result<models::ErrorList, Error<GetEventErrorsError>> {
106    // add a prefix to parameters to efficiently prevent name collisions
107    let p_limit = limit;
108
109    let uri_str = format!("{}/v1/event/errors", configuration.base_path);
110    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
111
112    if let Some(ref param_value) = p_limit {
113        req_builder = req_builder.query(&[("limit", &param_value.to_string())]);
114    }
115    if let Some(ref user_agent) = configuration.user_agent {
116        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
117    }
118    if let Some(ref token) = configuration.bearer_access_token {
119        req_builder = req_builder.bearer_auth(token.to_owned());
120    };
121
122    let req = req_builder.build()?;
123    let resp = configuration.client.execute(req).await?;
124
125    let status = resp.status();
126    let content_type = resp
127        .headers()
128        .get("content-type")
129        .and_then(|v| v.to_str().ok())
130        .unwrap_or("application/octet-stream");
131    let content_type = super::ContentType::from(content_type);
132
133    if !status.is_client_error() && !status.is_server_error() {
134        let content = resp.text().await?;
135        match content_type {
136            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
137            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::ErrorList`"))),
138            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::ErrorList`")))),
139        }
140    } else {
141        let content = resp.text().await?;
142        let entity: Option<GetEventErrorsError> = serde_json::from_str(&content).ok();
143        Err(Error::ResponseError(ResponseContent { status, content, entity }))
144    }
145}
146
147/// Health reports whether the event plane can take a write and the warehouse can answer a read.  It reports the analytics subsystem's own liveness in BOTH directions: plane is the event plane it WRITES (the bus and the JetStream stream every accepted event is published to, both named in the report), and datastore is the warehouse it READS, with each read lens's table reported as it is provisioned (the LLM usage ledger and the product-event table).  EITHER ONE DOWN IS A 503, and the report says WHICH — they are probed independently and never collapse into a single bit. This endpoint used to report the read half only, and answered 200/ok while every POST /v1/event failed on a stream that could not bind: a total ingest outage behind a green probe. A readiness gate here now gates on the write path too.  plane.ready IS A REAL PROBE and walks the ingest path itself — the same connection and the same stream a publish uses — so it cannot answer ready while a publish would 503. plane.reason carries the plane's own error text when it is false.  datastore IS NOT PROBED WITH A QUERY. It is the state of the process's own shared client — established, and not since closed — so a warehouse accepting connections and failing reads still reports true. Degraded CARRIES the report (status, the failing half, reason) as its body rather than an error envelope, so a gate reads the cause off the same object it got at 200.  A MISSING LENS TABLE IS NOT A FAILURE and never moves the status: a lens reported available:false answers honest-empty rather than erroring, so a fresh deployment whose collector has not emitted yet is legitimately 200 with the product-event lens unavailable. The lens block is reported whenever the warehouse is REACHABLE — including on a report degraded by the plane, where the tables genuinely were probed — and is absent only when the warehouse is not, having nothing to say about tables it could not reach.  Unauthenticated on purpose — liveness has to be probe-able — and it reads NO tenant data: table existence and stream presence only, never a row and never an event.
148pub async fn get_event_health(configuration: &configuration::Configuration, ) -> Result<models::HealthReport, Error<GetEventHealthError>> {
149
150    let uri_str = format!("{}/v1/event/health", configuration.base_path);
151    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
152
153    if let Some(ref user_agent) = configuration.user_agent {
154        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
155    }
156    if let Some(ref token) = configuration.bearer_access_token {
157        req_builder = req_builder.bearer_auth(token.to_owned());
158    };
159
160    let req = req_builder.build()?;
161    let resp = configuration.client.execute(req).await?;
162
163    let status = resp.status();
164    let content_type = resp
165        .headers()
166        .get("content-type")
167        .and_then(|v| v.to_str().ok())
168        .unwrap_or("application/octet-stream");
169    let content_type = super::ContentType::from(content_type);
170
171    if !status.is_client_error() && !status.is_server_error() {
172        let content = resp.text().await?;
173        match content_type {
174            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
175            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::HealthReport`"))),
176            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::HealthReport`")))),
177        }
178    } else {
179        let content = resp.text().await?;
180        let entity: Option<GetEventHealthError> = serde_json::from_str(&content).ok();
181        Err(Error::ResponseError(ResponseContent { status, content, entity }))
182    }
183}
184
185/// Returns the caller org's most recent product events, newest first. The console's raw-event view over event.event — the same table the capture endpoints fill — one row per stored event, with the row's attributes returned as the properties object.  The org is the validated principal's — never a parameter — and a read requires a real bearer, never the write-only publishable key. 403 without a validated bearer, 503 when the warehouse is unreachable.
186pub async fn get_event_insights_events(configuration: &configuration::Configuration, limit: Option<i32>) -> Result<models::EventList, Error<GetEventInsightsEventsError>> {
187    // add a prefix to parameters to efficiently prevent name collisions
188    let p_limit = limit;
189
190    let uri_str = format!("{}/v1/event/insights/events", configuration.base_path);
191    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
192
193    if let Some(ref param_value) = p_limit {
194        req_builder = req_builder.query(&[("limit", &param_value.to_string())]);
195    }
196    if let Some(ref user_agent) = configuration.user_agent {
197        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
198    }
199    if let Some(ref token) = configuration.bearer_access_token {
200        req_builder = req_builder.bearer_auth(token.to_owned());
201    };
202
203    let req = req_builder.build()?;
204    let resp = configuration.client.execute(req).await?;
205
206    let status = resp.status();
207    let content_type = resp
208        .headers()
209        .get("content-type")
210        .and_then(|v| v.to_str().ok())
211        .unwrap_or("application/octet-stream");
212    let content_type = super::ContentType::from(content_type);
213
214    if !status.is_client_error() && !status.is_server_error() {
215        let content = resp.text().await?;
216        match content_type {
217            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
218            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::EventList`"))),
219            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::EventList`")))),
220        }
221    } else {
222        let content = resp.text().await?;
223        let entity: Option<GetEventInsightsEventsError> = serde_json::from_str(&content).ok();
224        Err(Error::ResponseError(ResponseContent { status, content, entity }))
225    }
226}
227
228/// Reports that the unified insights surface is serving. It reads no tenant data and consults no dependency, so it answers 200 unconditionally and needs no principal — liveness must be probe-able. The warehouse-connectivity probe is a different question and lives at GET /v1/event/health.
229pub async fn get_event_insights_health(configuration: &configuration::Configuration, ) -> Result<models::InsightsStatus, Error<GetEventInsightsHealthError>> {
230
231    let uri_str = format!("{}/v1/event/insights/health", configuration.base_path);
232    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
233
234    if let Some(ref user_agent) = configuration.user_agent {
235        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
236    }
237    if let Some(ref token) = configuration.bearer_access_token {
238        req_builder = req_builder.bearer_auth(token.to_owned());
239    };
240
241    let req = req_builder.build()?;
242    let resp = configuration.client.execute(req).await?;
243
244    let status = resp.status();
245    let content_type = resp
246        .headers()
247        .get("content-type")
248        .and_then(|v| v.to_str().ok())
249        .unwrap_or("application/octet-stream");
250    let content_type = super::ContentType::from(content_type);
251
252    if !status.is_client_error() && !status.is_server_error() {
253        let content = resp.text().await?;
254        match content_type {
255            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
256            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::InsightsStatus`"))),
257            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::InsightsStatus`")))),
258        }
259    } else {
260        let content = resp.text().await?;
261        let entity: Option<GetEventInsightsHealthError> = serde_json::from_str(&content).ok();
262        Err(Error::ResponseError(ResponseContent { status, content, entity }))
263    }
264}
265
266/// Overview returns the caller org's analytics KPIs for one time window. Three lenses over one warehouse: llm is the live per-org LLM usage ledger (requests, tokens, spend, models, providers, errors) and is always real; web (pageviews, visitors, sessions) and commerce (orders, revenue, AOV) read the product-event table and report available=false rather than fabricating zeros when it holds nothing yet.  The org is the validated principal's — never a parameter — so a caller can only ever read its own tenant. 403 without a validated bearer, 400 on an unknown range, 503 when the warehouse is unreachable.
267pub async fn get_event_overview(configuration: &configuration::Configuration, range: Option<&str>, start: Option<&str>, end: Option<&str>) -> Result<models::Overview, Error<GetEventOverviewError>> {
268    // add a prefix to parameters to efficiently prevent name collisions
269    let p_range = range;
270    let p_start = start;
271    let p_end = end;
272
273    let uri_str = format!("{}/v1/event/overview", configuration.base_path);
274    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
275
276    if let Some(ref param_value) = p_range {
277        req_builder = req_builder.query(&[("range", &param_value.to_string())]);
278    }
279    if let Some(ref param_value) = p_start {
280        req_builder = req_builder.query(&[("start", &param_value.to_string())]);
281    }
282    if let Some(ref param_value) = p_end {
283        req_builder = req_builder.query(&[("end", &param_value.to_string())]);
284    }
285    if let Some(ref user_agent) = configuration.user_agent {
286        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
287    }
288    if let Some(ref token) = configuration.bearer_access_token {
289        req_builder = req_builder.bearer_auth(token.to_owned());
290    };
291
292    let req = req_builder.build()?;
293    let resp = configuration.client.execute(req).await?;
294
295    let status = resp.status();
296    let content_type = resp
297        .headers()
298        .get("content-type")
299        .and_then(|v| v.to_str().ok())
300        .unwrap_or("application/octet-stream");
301    let content_type = super::ContentType::from(content_type);
302
303    if !status.is_client_error() && !status.is_server_error() {
304        let content = resp.text().await?;
305        match content_type {
306            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
307            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::Overview`"))),
308            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Overview`")))),
309        }
310    } else {
311        let content = resp.text().await?;
312        let entity: Option<GetEventOverviewError> = serde_json::from_str(&content).ok();
313        Err(Error::ResponseError(ResponseContent { status, content, entity }))
314    }
315}
316
317/// Serves the browser tag that autocaptures pageviews (initial and SPA) and uncaught errors onto the canonical wire at POST /v1/event.  Install is one line, and it is the same line for a Hanzo property and for a customer's own page:      <script defer src=\"https://api.hanzo.ai/v1/event/tag.js\" data-key=\"pk-…\"></script>  `data-key` is the publishable key the project mints; `data-product` optionally names the emitting surface. The key may also ride the src as `?key=` for a host that strips data attributes.  WITHOUT A KEY THE TAG SENDS NOTHING. A keyless beacon is accepted 200 into $public, a reserved tenant the owning org cannot read — so silence is the honest failure, and the tag picks it rather than reporting success into a tenant nobody reads.
318pub async fn get_event_tag_period_js(configuration: &configuration::Configuration, ) -> Result<reqwest::Response, Error<GetEventTagPeriodJsError>> {
319
320    let uri_str = format!("{}/v1/event/tag.js", configuration.base_path);
321    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
322
323    if let Some(ref user_agent) = configuration.user_agent {
324        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
325    }
326    if let Some(ref token) = configuration.bearer_access_token {
327        req_builder = req_builder.bearer_auth(token.to_owned());
328    };
329
330    let req = req_builder.build()?;
331    let resp = configuration.client.execute(req).await?;
332
333    let status = resp.status();
334
335    if !status.is_client_error() && !status.is_server_error() {
336        Ok(resp)
337    } else {
338        let content = resp.text().await?;
339        let entity: Option<GetEventTagPeriodJsError> = serde_json::from_str(&content).ok();
340        Err(Error::ResponseError(ResponseContent { status, content, entity }))
341    }
342}
343
344/// Timeseries returns the caller org's LLM usage over time as an evenly-spaced series. One point per hour or per day — the bucket the window implies, 24h giving hours and 7d/30d giving days — carrying requests, total tokens and spend in cents. Empty buckets are filled with zeros so a client charts a continuous line.  The org is the validated principal's — never a parameter. 403 without a validated bearer, 400 on an unknown range, 503 when the warehouse is unreachable.
345pub async fn get_event_timeseries(configuration: &configuration::Configuration, range: Option<&str>, start: Option<&str>, end: Option<&str>) -> Result<models::Timeseries, Error<GetEventTimeseriesError>> {
346    // add a prefix to parameters to efficiently prevent name collisions
347    let p_range = range;
348    let p_start = start;
349    let p_end = end;
350
351    let uri_str = format!("{}/v1/event/timeseries", configuration.base_path);
352    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
353
354    if let Some(ref param_value) = p_range {
355        req_builder = req_builder.query(&[("range", &param_value.to_string())]);
356    }
357    if let Some(ref param_value) = p_start {
358        req_builder = req_builder.query(&[("start", &param_value.to_string())]);
359    }
360    if let Some(ref param_value) = p_end {
361        req_builder = req_builder.query(&[("end", &param_value.to_string())]);
362    }
363    if let Some(ref user_agent) = configuration.user_agent {
364        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
365    }
366    if let Some(ref token) = configuration.bearer_access_token {
367        req_builder = req_builder.bearer_auth(token.to_owned());
368    };
369
370    let req = req_builder.build()?;
371    let resp = configuration.client.execute(req).await?;
372
373    let status = resp.status();
374    let content_type = resp
375        .headers()
376        .get("content-type")
377        .and_then(|v| v.to_str().ok())
378        .unwrap_or("application/octet-stream");
379    let content_type = super::ContentType::from(content_type);
380
381    if !status.is_client_error() && !status.is_server_error() {
382        let content = resp.text().await?;
383        match content_type {
384            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
385            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::Timeseries`"))),
386            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Timeseries`")))),
387        }
388    } else {
389        let content = resp.text().await?;
390        let entity: Option<GetEventTimeseriesError> = serde_json::from_str(&content).ok();
391        Err(Error::ResponseError(ResponseContent { status, content, entity }))
392    }
393}
394
395/// Top returns the caller org's ranked lenses for one window, five of them at once. models ranks LLM models by spend and is always real; products ranks commerce orders by revenue; topPages ranks requested paths, topReferrers the external referrer domains (\"(direct)\" for a missing or same-origin one) and topSources the utm_source campaigns (\"(none)\" when absent), each by pageviews. Every lens carries each row's share of the in-window total, so a top-N honestly shows the long tail.  The four event lenses report available=false rather than fabricating zeros when the product-event table holds nothing yet. The org is the validated principal's — never a parameter. 403 without a validated bearer, 400 on an unknown range, 503 when the warehouse is unreachable.
396pub async fn get_event_top(configuration: &configuration::Configuration, range: Option<&str>, start: Option<&str>, end: Option<&str>, limit: Option<i32>) -> Result<models::Top, Error<GetEventTopError>> {
397    // add a prefix to parameters to efficiently prevent name collisions
398    let p_range = range;
399    let p_start = start;
400    let p_end = end;
401    let p_limit = limit;
402
403    let uri_str = format!("{}/v1/event/top", configuration.base_path);
404    let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
405
406    if let Some(ref param_value) = p_range {
407        req_builder = req_builder.query(&[("range", &param_value.to_string())]);
408    }
409    if let Some(ref param_value) = p_start {
410        req_builder = req_builder.query(&[("start", &param_value.to_string())]);
411    }
412    if let Some(ref param_value) = p_end {
413        req_builder = req_builder.query(&[("end", &param_value.to_string())]);
414    }
415    if let Some(ref param_value) = p_limit {
416        req_builder = req_builder.query(&[("limit", &param_value.to_string())]);
417    }
418    if let Some(ref user_agent) = configuration.user_agent {
419        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
420    }
421    if let Some(ref token) = configuration.bearer_access_token {
422        req_builder = req_builder.bearer_auth(token.to_owned());
423    };
424
425    let req = req_builder.build()?;
426    let resp = configuration.client.execute(req).await?;
427
428    let status = resp.status();
429    let content_type = resp
430        .headers()
431        .get("content-type")
432        .and_then(|v| v.to_str().ok())
433        .unwrap_or("application/octet-stream");
434    let content_type = super::ContentType::from(content_type);
435
436    if !status.is_client_error() && !status.is_server_error() {
437        let content = resp.text().await?;
438        match content_type {
439            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
440            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::Top`"))),
441            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Top`")))),
442        }
443    } else {
444        let content = resp.text().await?;
445        let entity: Option<GetEventTopError> = serde_json::from_str(&content).ok();
446        Err(Error::ResponseError(ResponseContent { status, content, entity }))
447    }
448}
449
450/// Stores pageviews, browser errors, identifies and custom commerce events as rows in the caller's own tenant, and answers a receipt {accepted, dropped} that always totals what was sent — a beacon is never silently discarded.  THE STATUS SAYS WHETHER ANYTHING LANDED, so a green check can never mean an empty warehouse. 200 means at least one event was stored (or that nothing was sent), and a nonzero `dropped` beside a nonzero `accepted` is a PARTIAL batch, never a failed one — a batch is not refused whole for its worst element. If NOTHING was stored the request is an error, and it names the one thing that fixes it: 401 `ingest_key_required` when every event was refused for want of a credential (the same events land with a key), and 400 `unroutable_events` when the caller HAD capability and the body still named nothing storable.  ONE endpoint for every wire a Hanzo surface emits, dispatched by the SHAPE of the body and never by a second path: a bare event object, a bare array of them, the {batch:[…]} / {events:[…]} envelope, the team console's snake_case array, and the PostHog wire (spelled `distinct_id`/`api_key`, which the canonical wire never uses). BATCH IS A BODY, NOT A PATH — there is no /v1/event/batch, because an array already is one.  WHAT THE CALLER PRESENTS DECIDES WHAT IT MAY WRITE, and the endpoint itself grants nothing. A validated bearer or an org API key writes the full event at full fidelity. A PUBLISHABLE key (pk-, on Authorization: Bearer, x-hanzo-ingest-key, or ?ingest_key= for navigator.sendBeacon, which cannot set headers) does the same, and is the credential a browser bundle ships: it is deliberately NOT a secret, it resolves WHICH tenant a beacon belongs to and nothing more. A pk- never authenticates and can READ NOTHING — not this org's errors, not a lens, not any other route on this API — so a leaked one lets a stranger write into your stream, and never lets one read out of it. Reading these rows back always takes a real bearer. A Hanzo Team workspace token resolves its org at REDUCED capability: the signed account names the person, so a `distinctId` in the body cannot pin events on a colleague.  NO CREDENTIAL IS REFUSED: a write the server cannot attribute to a project is 401 `ingest_key_required`, and a credential that IS presented but resolves to no project is 403 `ingest_key_unknown`. Nothing is filed under a shared tenant — events nobody can read are worse than events nobody sent, because the caller is told it succeeded. A browser bundle therefore always ships a pk-, which is what /v1/event/tag.js takes.  A REDUCED principal — a Hanzo Team workspace token — writes through the PROJECTION into its own org: narrowed to what the SERVER can name (pageviews and errors, plus the closed autocapture vocabulary $click, $input, $change, $submit, $view), where every one of those names is resolved through a server-owned table and stored as that table's value, so the name on the wire is never the name in the row. Stripped, too, to the fields the projection names, so revenue, personId, groupId and every property but the element annotation cannot reach a row — and an exception is carried only on an error, never on an interaction, so a click cannot ship a stack trace into a row's attributes. It does NOT name the person: the signed account is the identity, so a `distinctId` in the body cannot pin events on a colleague. Everything refused is counted in `dropped`.  The projected lane alone is bounded: 413 over 64 KiB, 400 over 50 events, 429 on the per-client-IP and per-peer caps, and a DNT:1 or Sec-GPC:1 request stores nothing and says so in the receipt. Two stored values carry their own bounds on top, because a request cap does not bound one value: an element annotation over 2 KiB (or a trail over 32 steps) and an exception class over 256 bytes are dropped from the row, which still lands. Authenticated bodies are offered to the observability plane first, which claims LLM-observability ingestion batches and declines everything else.
451pub async fn post_event(configuration: &configuration::Configuration, post_event_request: Option<models::PostEventRequest>) -> Result<models::CaptureResult, Error<PostEventError>> {
452    // add a prefix to parameters to efficiently prevent name collisions
453    let p_post_event_request = post_event_request;
454
455    let uri_str = format!("{}/v1/event", configuration.base_path);
456    let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
457
458    if let Some(ref user_agent) = configuration.user_agent {
459        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
460    }
461    if let Some(ref token) = configuration.bearer_access_token {
462        req_builder = req_builder.bearer_auth(token.to_owned());
463    };
464    req_builder = req_builder.json(&p_post_event_request);
465
466    let req = req_builder.build()?;
467    let resp = configuration.client.execute(req).await?;
468
469    let status = resp.status();
470    let content_type = resp
471        .headers()
472        .get("content-type")
473        .and_then(|v| v.to_str().ok())
474        .unwrap_or("application/octet-stream");
475    let content_type = super::ContentType::from(content_type);
476
477    if !status.is_client_error() && !status.is_server_error() {
478        let content = resp.text().await?;
479        match content_type {
480            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
481            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::CaptureResult`"))),
482            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::CaptureResult`")))),
483        }
484    } else {
485        let content = resp.text().await?;
486        let entity: Option<PostEventError> = serde_json::from_str(&content).ok();
487        Err(Error::ResponseError(ResponseContent { status, content, entity }))
488    }
489}
490
491/// Accepts the CURRENT Sentry wire — the framed envelope a modern SDK posts, carrying its items in one request — so an application already instrumented with Sentry reports into Hanzo's error tracking by pointing its DSN here and changing nothing else.  CLOUD ROUTES IT AND READS NONE OF IT. The body is relayed byte-for-byte to the observability plane, which parses the wire, verifies the credential and answers; this endpoint declares no response shape because it does not know one. A deployment with no observability plane mounted answers 503.  THE CREDENTIAL IS A SENTRY DSN KEY, NOT A HANZO PRINCIPAL. This is one of the few writes on the platform that carries no bearer and no org header by design — a Sentry SDK has neither — and it is exempt from the principal gate for that reason. The observability plane verifies the DSN key itself, fail-closed: a request without a valid one is refused there, never admitted here. Presenting a Hanzo bearer instead does nothing.  `project` IS THE DSN'S PROJECT ID — the identifier in the DSN the SDK was configured with, and what the tenant is derived from. It is NOT a Hanzo IAM project and NOT a todo project key. Only these two ingest paths map through: no observability READ API is reachable by any other suffix under this prefix.
492pub async fn post_event_by_project_envelope(configuration: &configuration::Configuration, project: &str, body: Option<Vec<u8>>) -> Result<(), Error<PostEventByProjectEnvelopeError>> {
493    // add a prefix to parameters to efficiently prevent name collisions
494    let p_project = project;
495    let p_body = body;
496
497    let uri_str = format!("{}/v1/event/{project}/envelope", configuration.base_path, project=crate::apis::urlencode(p_project));
498    let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
499
500    if let Some(ref user_agent) = configuration.user_agent {
501        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
502    }
503    if let Some(ref token) = configuration.bearer_access_token {
504        req_builder = req_builder.bearer_auth(token.to_owned());
505    };
506    if let Some(body) = p_body {
507        req_builder = req_builder.body(body);
508    }
509
510    let req = req_builder.build()?;
511    let resp = configuration.client.execute(req).await?;
512
513    let status = resp.status();
514
515    if !status.is_client_error() && !status.is_server_error() {
516        Ok(())
517    } else {
518        let content = resp.text().await?;
519        let entity: Option<PostEventByProjectEnvelopeError> = serde_json::from_str(&content).ok();
520        Err(Error::ResponseError(ResponseContent { status, content, entity }))
521    }
522}
523
524/// Accepts the LEGACY Sentry wire: one event per request, what an SDK predating envelopes sends. Same handler, same credential, same destination as the envelope endpoint — kept open so an old client reports without being upgraded first. New instrumentation has no reason to choose it.  CLOUD ROUTES IT AND READS NONE OF IT. The body is relayed byte-for-byte to the observability plane, which parses the wire, verifies the credential and answers; this endpoint declares no response shape because it does not know one. A deployment with no observability plane mounted answers 503.  THE CREDENTIAL IS A SENTRY DSN KEY, NOT A HANZO PRINCIPAL. This is one of the few writes on the platform that carries no bearer and no org header by design — a Sentry SDK has neither — and it is exempt from the principal gate for that reason. The observability plane verifies the DSN key itself, fail-closed: a request without a valid one is refused there, never admitted here. Presenting a Hanzo bearer instead does nothing.  `project` IS THE DSN'S PROJECT ID — the identifier in the DSN the SDK was configured with, and what the tenant is derived from. It is NOT a Hanzo IAM project and NOT a todo project key. Only these two ingest paths map through: no observability READ API is reachable by any other suffix under this prefix.
525pub async fn post_event_by_project_store(configuration: &configuration::Configuration, project: &str, body: Option<Vec<u8>>) -> Result<(), Error<PostEventByProjectStoreError>> {
526    // add a prefix to parameters to efficiently prevent name collisions
527    let p_project = project;
528    let p_body = body;
529
530    let uri_str = format!("{}/v1/event/{project}/store", configuration.base_path, project=crate::apis::urlencode(p_project));
531    let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
532
533    if let Some(ref user_agent) = configuration.user_agent {
534        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
535    }
536    if let Some(ref token) = configuration.bearer_access_token {
537        req_builder = req_builder.bearer_auth(token.to_owned());
538    };
539    if let Some(body) = p_body {
540        req_builder = req_builder.body(body);
541    }
542
543    let req = req_builder.build()?;
544    let resp = configuration.client.execute(req).await?;
545
546    let status = resp.status();
547
548    if !status.is_client_error() && !status.is_server_error() {
549        Ok(())
550    } else {
551        let content = resp.text().await?;
552        let entity: Option<PostEventByProjectStoreError> = serde_json::from_str(&content).ok();
553        Err(Error::ResponseError(ResponseContent { status, content, entity }))
554    }
555}
556
557/// Accepts a batch of rrweb events from a browser recorder and hands it to the session-replay pipeline, which stores the recording and derives the session summary a player reads back.  ONE REQUEST IS ONE BATCH, and it is all-or-nothing: the recording is made durable before this answers, so a 200 {\"accepted\":1} means stored and never \"buffered somewhere\". There is no partial count, because a half-written recording is not a recording.  `sessionId` is REQUIRED and bounded — at most 70 characters of ASCII letters, digits or '-'. It is the key every batch of one visit is grouped and ordered by, so an id outside that grammar is refused 400 here rather than accepted and dropped further down. `windowId` separates two tabs of one session and `distinctId` attributes the recording to a person; both are optional. `events` is the rrweb batch, each element a raw eventWithTime object, carried VERBATIM — the summary (click, keypress and mouse-activity counts, size) is derived downstream from exactly these bytes, so nothing is re-encoded or dropped.  THE CALLER'S CREDENTIAL DECIDES THE TENANT, and the body never does: the recording lands in the org the presented credential resolves to. It takes the SAME credentials as /v1/event — a validated bearer, an org API key, or a publishable pk- key on Authorization: Bearer, x-hanzo-ingest-key or ?ingest_key= — so a browser bundle already holding a pk- for events needs nothing new to record. A caller that presents nothing is 401 `ingest_key_required`; one whose key resolves to no project is 403 `ingest_key_unknown`; a reduced principal (a Hanzo Team workspace token) is 403 `insufficient_capability`, because a full-fidelity screen recording has no projected form that is safe for a guest to write into a host org.  BOUNDS: 413 over 512 KiB of body, and that is the only bound on one batch — a recorder is expected to chunk a long session rather than send it whole, and the cap is the size one message can carry rather than an arbitrary number. 503 when the pipeline cannot take the batch: honest unavailability the caller can retry, never a 200 over a discarded recording.
558pub async fn post_event_replay(configuration: &configuration::Configuration, replay_body: Option<models::ReplayBody>) -> Result<models::CaptureResult, Error<PostEventReplayError>> {
559    // add a prefix to parameters to efficiently prevent name collisions
560    let p_replay_body = replay_body;
561
562    let uri_str = format!("{}/v1/event/replay", configuration.base_path);
563    let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
564
565    if let Some(ref user_agent) = configuration.user_agent {
566        req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
567    }
568    if let Some(ref token) = configuration.bearer_access_token {
569        req_builder = req_builder.bearer_auth(token.to_owned());
570    };
571    req_builder = req_builder.json(&p_replay_body);
572
573    let req = req_builder.build()?;
574    let resp = configuration.client.execute(req).await?;
575
576    let status = resp.status();
577    let content_type = resp
578        .headers()
579        .get("content-type")
580        .and_then(|v| v.to_str().ok())
581        .unwrap_or("application/octet-stream");
582    let content_type = super::ContentType::from(content_type);
583
584    if !status.is_client_error() && !status.is_server_error() {
585        let content = resp.text().await?;
586        match content_type {
587            ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
588            ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::CaptureResult`"))),
589            ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::CaptureResult`")))),
590        }
591    } else {
592        let content = resp.text().await?;
593        let entity: Option<PostEventReplayError> = serde_json::from_str(&content).ok();
594        Err(Error::ResponseError(ResponseContent { status, content, entity }))
595    }
596}
597