1use super::ips_file::IpsFile;
2use crate::api::CloudflareIpCountryHeader;
3use crate::clickhouse::write_to_table;
4use crate::{api::slack, AppState, Error, Result};
5use anyhow::{anyhow, Context};
6use aws_sdk_s3::primitives::ByteStream;
7use axum::{
8 body::Bytes,
9 headers::Header,
10 http::{HeaderMap, HeaderName, StatusCode},
11 routing::post,
12 Extension, Router, TypedHeader,
13};
14use rpc::ExtensionMetadata;
15use semantic_version::SemanticVersion;
16use serde::{Serialize, Serializer};
17use sha2::{Digest, Sha256};
18use std::sync::{Arc, OnceLock};
19use telemetry_events::{
20 ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
21 EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, Panic,
22 ReplEvent, SettingEvent,
23};
24use uuid::Uuid;
25
26const CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
27
28pub fn router() -> Router {
29 Router::new()
30 .route("/telemetry/events", post(post_events))
31 .route("/telemetry/crashes", post(post_crash))
32 .route("/telemetry/panics", post(post_panic))
33 .route("/telemetry/hangs", post(post_hang))
34}
35
36pub struct ZedChecksumHeader(Vec<u8>);
37
38impl Header for ZedChecksumHeader {
39 fn name() -> &'static HeaderName {
40 static ZED_CHECKSUM_HEADER: OnceLock<HeaderName> = OnceLock::new();
41 ZED_CHECKSUM_HEADER.get_or_init(|| HeaderName::from_static("x-zed-checksum"))
42 }
43
44 fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
45 where
46 Self: Sized,
47 I: Iterator<Item = &'i axum::http::HeaderValue>,
48 {
49 let checksum = values
50 .next()
51 .ok_or_else(axum::headers::Error::invalid)?
52 .to_str()
53 .map_err(|_| axum::headers::Error::invalid())?;
54
55 let bytes = hex::decode(checksum).map_err(|_| axum::headers::Error::invalid())?;
56 Ok(Self(bytes))
57 }
58
59 fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
60 unimplemented!()
61 }
62}
63
64pub async fn post_crash(
65 Extension(app): Extension<Arc<AppState>>,
66 headers: HeaderMap,
67 body: Bytes,
68) -> Result<()> {
69 let report = IpsFile::parse(&body)?;
70 let version_threshold = SemanticVersion::new(0, 123, 0);
71
72 let bundle_id = &report.header.bundle_id;
73 let app_version = &report.app_version();
74
75 if bundle_id == "dev.zed.Zed-Dev" {
76 log::error!("Crash uploads from {} are ignored.", bundle_id);
77 return Ok(());
78 }
79
80 if app_version.is_none() || app_version.unwrap() < version_threshold {
81 log::error!(
82 "Crash uploads from {} are ignored.",
83 report.header.app_version
84 );
85 return Ok(());
86 }
87 let app_version = app_version.unwrap();
88
89 if let Some(blob_store_client) = app.blob_store_client.as_ref() {
90 let response = blob_store_client
91 .head_object()
92 .bucket(CRASH_REPORTS_BUCKET)
93 .key(report.header.incident_id.clone() + ".ips")
94 .send()
95 .await;
96
97 if response.is_ok() {
98 log::info!("We've already uploaded this crash");
99 return Ok(());
100 }
101
102 blob_store_client
103 .put_object()
104 .bucket(CRASH_REPORTS_BUCKET)
105 .key(report.header.incident_id.clone() + ".ips")
106 .acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
107 .body(ByteStream::from(body.to_vec()))
108 .send()
109 .await
110 .map_err(|e| log::error!("Failed to upload crash: {}", e))
111 .ok();
112 }
113
114 let recent_panic_on: Option<i64> = headers
115 .get("x-zed-panicked-on")
116 .and_then(|h| h.to_str().ok())
117 .and_then(|s| s.parse().ok());
118
119 let installation_id = headers
120 .get("x-zed-installation-id")
121 .and_then(|h| h.to_str().ok())
122 .map(|s| s.to_string())
123 .unwrap_or_default();
124
125 let mut recent_panic = None;
126
127 if let Some(recent_panic_on) = recent_panic_on {
128 let crashed_at = match report.timestamp() {
129 Ok(t) => Some(t),
130 Err(e) => {
131 log::error!("Can't parse {}: {}", report.header.timestamp, e);
132 None
133 }
134 };
135 if crashed_at.is_some_and(|t| (t.timestamp_millis() - recent_panic_on).abs() <= 30000) {
136 recent_panic = headers.get("x-zed-panic").and_then(|h| h.to_str().ok());
137 }
138 }
139
140 let description = report.description(recent_panic);
141 let summary = report.backtrace_summary();
142
143 tracing::error!(
144 service = "client",
145 version = %report.header.app_version,
146 os_version = %report.header.os_version,
147 bundle_id = %report.header.bundle_id,
148 incident_id = %report.header.incident_id,
149 installation_id = %installation_id,
150 description = %description,
151 backtrace = %summary,
152 "crash report"
153 );
154
155 if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
156 let payload = slack::WebhookBody::new(|w| {
157 w.add_section(|s| s.text(slack::Text::markdown(description)))
158 .add_section(|s| {
159 s.add_field(slack::Text::markdown(format!(
160 "*Version:*\n{} ({})",
161 bundle_id, app_version
162 )))
163 .add_field({
164 let hostname = app.config.blob_store_url.clone().unwrap_or_default();
165 let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
166 hostname.strip_prefix("http://").unwrap_or_default()
167 });
168
169 slack::Text::markdown(format!(
170 "*Incident:*\n<https://{}.{}/{}.ips|{}…>",
171 CRASH_REPORTS_BUCKET,
172 hostname,
173 report.header.incident_id,
174 report
175 .header
176 .incident_id
177 .chars()
178 .take(8)
179 .collect::<String>(),
180 ))
181 })
182 })
183 .add_rich_text(|r| r.add_preformatted(|p| p.add_text(summary)))
184 });
185 let payload_json = serde_json::to_string(&payload).map_err(|err| {
186 log::error!("Failed to serialize payload to JSON: {err}");
187 Error::Internal(anyhow!(err))
188 })?;
189
190 reqwest::Client::new()
191 .post(slack_panics_webhook)
192 .header("Content-Type", "application/json")
193 .body(payload_json)
194 .send()
195 .await
196 .map_err(|err| {
197 log::error!("Failed to send payload to Slack: {err}");
198 Error::Internal(anyhow!(err))
199 })?;
200 }
201
202 Ok(())
203}
204
205pub async fn post_hang(
206 Extension(app): Extension<Arc<AppState>>,
207 TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
208 body: Bytes,
209) -> Result<()> {
210 let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
211 return Err(Error::http(
212 StatusCode::INTERNAL_SERVER_ERROR,
213 "events not enabled".into(),
214 ))?;
215 };
216
217 if checksum != expected {
218 return Err(Error::http(
219 StatusCode::BAD_REQUEST,
220 "invalid checksum".into(),
221 ))?;
222 }
223
224 let incident_id = Uuid::new_v4().to_string();
225
226 // dump JSON into S3 so we can get frame offsets if we need to.
227 if let Some(blob_store_client) = app.blob_store_client.as_ref() {
228 blob_store_client
229 .put_object()
230 .bucket(CRASH_REPORTS_BUCKET)
231 .key(incident_id.clone() + ".hang.json")
232 .acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
233 .body(ByteStream::from(body.to_vec()))
234 .send()
235 .await
236 .map_err(|e| log::error!("Failed to upload crash: {}", e))
237 .ok();
238 }
239
240 let report: telemetry_events::HangReport = serde_json::from_slice(&body).map_err(|err| {
241 log::error!("can't parse report json: {err}");
242 Error::Internal(anyhow!(err))
243 })?;
244
245 let mut backtrace = "Possible hang detected on main thread:".to_string();
246 let unknown = "<unknown>".to_string();
247 for frame in report.backtrace.iter() {
248 backtrace.push_str(&format!("\n{}", frame.symbols.first().unwrap_or(&unknown)));
249 }
250
251 tracing::error!(
252 service = "client",
253 version = %report.app_version.unwrap_or_default().to_string(),
254 os_name = %report.os_name,
255 os_version = report.os_version.unwrap_or_default().to_string(),
256 incident_id = %incident_id,
257 installation_id = %report.installation_id.unwrap_or_default(),
258 backtrace = %backtrace,
259 "hang report");
260
261 Ok(())
262}
263
264pub async fn post_panic(
265 Extension(app): Extension<Arc<AppState>>,
266 TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
267 body: Bytes,
268) -> Result<()> {
269 let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
270 return Err(Error::http(
271 StatusCode::INTERNAL_SERVER_ERROR,
272 "events not enabled".into(),
273 ))?;
274 };
275
276 if checksum != expected {
277 return Err(Error::http(
278 StatusCode::BAD_REQUEST,
279 "invalid checksum".into(),
280 ))?;
281 }
282
283 let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
284 .map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
285 let panic = report.panic;
286
287 if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
288 return Err(Error::http(
289 StatusCode::BAD_REQUEST,
290 "invalid os version".into(),
291 ))?;
292 }
293
294 tracing::error!(
295 service = "client",
296 version = %panic.app_version,
297 os_name = %panic.os_name,
298 os_version = %panic.os_version.clone().unwrap_or_default(),
299 installation_id = %panic.installation_id.clone().unwrap_or_default(),
300 description = %panic.payload,
301 backtrace = %panic.backtrace.join("\n"),
302 "panic report"
303 );
304
305 let backtrace = if panic.backtrace.len() > 25 {
306 let total = panic.backtrace.len();
307 format!(
308 "{}\n and {} more",
309 panic
310 .backtrace
311 .iter()
312 .take(20)
313 .cloned()
314 .collect::<Vec<_>>()
315 .join("\n"),
316 total - 20
317 )
318 } else {
319 panic.backtrace.join("\n")
320 };
321
322 if !report_to_slack(&panic) {
323 return Ok(());
324 }
325
326 let backtrace_with_summary = panic.payload + "\n" + &backtrace;
327
328 if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
329 let payload = slack::WebhookBody::new(|w| {
330 w.add_section(|s| s.text(slack::Text::markdown("Panic request".to_string())))
331 .add_section(|s| {
332 s.add_field(slack::Text::markdown(format!(
333 "*Version:*\n {} ",
334 panic.app_version
335 )))
336 .add_field({
337 slack::Text::markdown(format!(
338 "*OS:*\n{} {}",
339 panic.os_name,
340 panic.os_version.unwrap_or_default()
341 ))
342 })
343 })
344 .add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace_with_summary)))
345 });
346 let payload_json = serde_json::to_string(&payload).map_err(|err| {
347 log::error!("Failed to serialize payload to JSON: {err}");
348 Error::Internal(anyhow!(err))
349 })?;
350
351 reqwest::Client::new()
352 .post(slack_panics_webhook)
353 .header("Content-Type", "application/json")
354 .body(payload_json)
355 .send()
356 .await
357 .map_err(|err| {
358 log::error!("Failed to send payload to Slack: {err}");
359 Error::Internal(anyhow!(err))
360 })?;
361 }
362
363 Ok(())
364}
365
366fn report_to_slack(panic: &Panic) -> bool {
367 if panic.payload.contains("ERROR_SURFACE_LOST_KHR") {
368 return false;
369 }
370
371 if panic.payload.contains("ERROR_INITIALIZATION_FAILED") {
372 return false;
373 }
374
375 if panic
376 .payload
377 .contains("GPU has crashed, and no debug information is available")
378 {
379 return false;
380 }
381
382 true
383}
384
385pub async fn post_events(
386 Extension(app): Extension<Arc<AppState>>,
387 TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
388 country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
389 body: Bytes,
390) -> Result<()> {
391 let Some(clickhouse_client) = app.clickhouse_client.clone() else {
392 Err(Error::http(
393 StatusCode::NOT_IMPLEMENTED,
394 "not supported".into(),
395 ))?
396 };
397
398 let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
399 return Err(Error::http(
400 StatusCode::INTERNAL_SERVER_ERROR,
401 "events not enabled".into(),
402 ))?;
403 };
404
405 let checksum_matched = checksum == expected;
406
407 let request_body: telemetry_events::EventRequestBody =
408 serde_json::from_slice(&body).map_err(|err| {
409 log::error!("can't parse event json: {err}");
410 Error::Internal(anyhow!(err))
411 })?;
412
413 let mut to_upload = ToUpload::default();
414 let Some(last_event) = request_body.events.last() else {
415 return Err(Error::http(StatusCode::BAD_REQUEST, "no events".into()))?;
416 };
417 let country_code = country_code_header.map(|h| h.to_string());
418
419 let first_event_at = chrono::Utc::now()
420 - chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
421
422 for wrapper in &request_body.events {
423 match &wrapper.event {
424 Event::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event(
425 event.clone(),
426 wrapper,
427 &request_body,
428 first_event_at,
429 country_code.clone(),
430 checksum_matched,
431 )),
432 // Needed for clients sending old copilot_event types
433 Event::Copilot(_) => {}
434 Event::InlineCompletion(event) => {
435 to_upload
436 .inline_completion_events
437 .push(InlineCompletionEventRow::from_event(
438 event.clone(),
439 wrapper,
440 &request_body,
441 first_event_at,
442 country_code.clone(),
443 checksum_matched,
444 ))
445 }
446 Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
447 event.clone(),
448 wrapper,
449 &request_body,
450 first_event_at,
451 checksum_matched,
452 )),
453 Event::Assistant(event) => {
454 to_upload
455 .assistant_events
456 .push(AssistantEventRow::from_event(
457 event.clone(),
458 wrapper,
459 &request_body,
460 first_event_at,
461 checksum_matched,
462 ))
463 }
464 Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event(
465 event.clone(),
466 wrapper,
467 &request_body,
468 first_event_at,
469 checksum_matched,
470 )),
471 Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event(
472 event.clone(),
473 wrapper,
474 &request_body,
475 first_event_at,
476 checksum_matched,
477 )),
478 Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
479 event.clone(),
480 wrapper,
481 &request_body,
482 first_event_at,
483 checksum_matched,
484 )),
485 Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event(
486 event.clone(),
487 wrapper,
488 &request_body,
489 first_event_at,
490 checksum_matched,
491 )),
492 Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
493 event.clone(),
494 wrapper,
495 &request_body,
496 first_event_at,
497 checksum_matched,
498 )),
499 Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
500 event.clone(),
501 wrapper,
502 &request_body,
503 first_event_at,
504 checksum_matched,
505 )),
506 Event::Extension(event) => {
507 let metadata = app
508 .db
509 .get_extension_version(&event.extension_id, &event.version)
510 .await?;
511 to_upload
512 .extension_events
513 .push(ExtensionEventRow::from_event(
514 event.clone(),
515 wrapper,
516 &request_body,
517 metadata,
518 first_event_at,
519 checksum_matched,
520 ))
521 }
522 Event::Repl(event) => to_upload.repl_events.push(ReplEventRow::from_event(
523 event.clone(),
524 wrapper,
525 &request_body,
526 first_event_at,
527 checksum_matched,
528 )),
529 }
530 }
531
532 to_upload
533 .upload(&clickhouse_client)
534 .await
535 .map_err(|err| Error::Internal(anyhow!(err)))?;
536
537 Ok(())
538}
539
540#[derive(Default)]
541struct ToUpload {
542 editor_events: Vec<EditorEventRow>,
543 inline_completion_events: Vec<InlineCompletionEventRow>,
544 assistant_events: Vec<AssistantEventRow>,
545 call_events: Vec<CallEventRow>,
546 cpu_events: Vec<CpuEventRow>,
547 memory_events: Vec<MemoryEventRow>,
548 app_events: Vec<AppEventRow>,
549 setting_events: Vec<SettingEventRow>,
550 extension_events: Vec<ExtensionEventRow>,
551 edit_events: Vec<EditEventRow>,
552 action_events: Vec<ActionEventRow>,
553 repl_events: Vec<ReplEventRow>,
554}
555
556impl ToUpload {
557 pub async fn upload(&self, clickhouse_client: &clickhouse::Client) -> anyhow::Result<()> {
558 const EDITOR_EVENTS_TABLE: &str = "editor_events";
559 write_to_table(EDITOR_EVENTS_TABLE, &self.editor_events, clickhouse_client)
560 .await
561 .with_context(|| format!("failed to upload to table '{EDITOR_EVENTS_TABLE}'"))?;
562
563 const INLINE_COMPLETION_EVENTS_TABLE: &str = "inline_completion_events";
564 write_to_table(
565 INLINE_COMPLETION_EVENTS_TABLE,
566 &self.inline_completion_events,
567 clickhouse_client,
568 )
569 .await
570 .with_context(|| format!("failed to upload to table '{INLINE_COMPLETION_EVENTS_TABLE}'"))?;
571
572 const ASSISTANT_EVENTS_TABLE: &str = "assistant_events";
573 write_to_table(
574 ASSISTANT_EVENTS_TABLE,
575 &self.assistant_events,
576 clickhouse_client,
577 )
578 .await
579 .with_context(|| format!("failed to upload to table '{ASSISTANT_EVENTS_TABLE}'"))?;
580
581 const CALL_EVENTS_TABLE: &str = "call_events";
582 write_to_table(CALL_EVENTS_TABLE, &self.call_events, clickhouse_client)
583 .await
584 .with_context(|| format!("failed to upload to table '{CALL_EVENTS_TABLE}'"))?;
585
586 const CPU_EVENTS_TABLE: &str = "cpu_events";
587 write_to_table(CPU_EVENTS_TABLE, &self.cpu_events, clickhouse_client)
588 .await
589 .with_context(|| format!("failed to upload to table '{CPU_EVENTS_TABLE}'"))?;
590
591 const MEMORY_EVENTS_TABLE: &str = "memory_events";
592 write_to_table(MEMORY_EVENTS_TABLE, &self.memory_events, clickhouse_client)
593 .await
594 .with_context(|| format!("failed to upload to table '{MEMORY_EVENTS_TABLE}'"))?;
595
596 const APP_EVENTS_TABLE: &str = "app_events";
597 write_to_table(APP_EVENTS_TABLE, &self.app_events, clickhouse_client)
598 .await
599 .with_context(|| format!("failed to upload to table '{APP_EVENTS_TABLE}'"))?;
600
601 const SETTING_EVENTS_TABLE: &str = "setting_events";
602 write_to_table(
603 SETTING_EVENTS_TABLE,
604 &self.setting_events,
605 clickhouse_client,
606 )
607 .await
608 .with_context(|| format!("failed to upload to table '{SETTING_EVENTS_TABLE}'"))?;
609
610 const EXTENSION_EVENTS_TABLE: &str = "extension_events";
611 write_to_table(
612 EXTENSION_EVENTS_TABLE,
613 &self.extension_events,
614 clickhouse_client,
615 )
616 .await
617 .with_context(|| format!("failed to upload to table '{EXTENSION_EVENTS_TABLE}'"))?;
618
619 const EDIT_EVENTS_TABLE: &str = "edit_events";
620 write_to_table(EDIT_EVENTS_TABLE, &self.edit_events, clickhouse_client)
621 .await
622 .with_context(|| format!("failed to upload to table '{EDIT_EVENTS_TABLE}'"))?;
623
624 const ACTION_EVENTS_TABLE: &str = "action_events";
625 write_to_table(ACTION_EVENTS_TABLE, &self.action_events, clickhouse_client)
626 .await
627 .with_context(|| format!("failed to upload to table '{ACTION_EVENTS_TABLE}'"))?;
628
629 const REPL_EVENTS_TABLE: &str = "repl_events";
630 write_to_table(REPL_EVENTS_TABLE, &self.repl_events, clickhouse_client)
631 .await
632 .with_context(|| format!("failed to upload to table '{REPL_EVENTS_TABLE}'"))?;
633
634 Ok(())
635 }
636}
637
638pub fn serialize_country_code<S>(country_code: &str, serializer: S) -> Result<S::Ok, S::Error>
639where
640 S: Serializer,
641{
642 if country_code.len() != 2 {
643 use serde::ser::Error;
644 return Err(S::Error::custom(
645 "country_code must be exactly 2 characters",
646 ));
647 }
648
649 let country_code = country_code.as_bytes();
650
651 serializer.serialize_u16(((country_code[1] as u16) << 8) + country_code[0] as u16)
652}
653
654#[derive(Serialize, Debug, clickhouse::Row)]
655pub struct EditorEventRow {
656 system_id: String,
657 installation_id: String,
658 session_id: Option<String>,
659 metrics_id: String,
660 operation: String,
661 app_version: String,
662 file_extension: String,
663 os_name: String,
664 os_version: String,
665 release_channel: String,
666 signed_in: bool,
667 vim_mode: bool,
668 #[serde(serialize_with = "serialize_country_code")]
669 country_code: String,
670 region_code: String,
671 city: String,
672 time: i64,
673 copilot_enabled: bool,
674 copilot_enabled_for_language: bool,
675 historical_event: bool,
676 architecture: String,
677 is_staff: Option<bool>,
678 major: Option<i32>,
679 minor: Option<i32>,
680 patch: Option<i32>,
681 checksum_matched: bool,
682}
683
684impl EditorEventRow {
685 fn from_event(
686 event: EditorEvent,
687 wrapper: &EventWrapper,
688 body: &EventRequestBody,
689 first_event_at: chrono::DateTime<chrono::Utc>,
690 country_code: Option<String>,
691 checksum_matched: bool,
692 ) -> Self {
693 let semver = body.semver();
694 let time =
695 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
696
697 Self {
698 app_version: body.app_version.clone(),
699 major: semver.map(|v| v.major() as i32),
700 minor: semver.map(|v| v.minor() as i32),
701 patch: semver.map(|v| v.patch() as i32),
702 checksum_matched,
703 release_channel: body.release_channel.clone().unwrap_or_default(),
704 os_name: body.os_name.clone(),
705 os_version: body.os_version.clone().unwrap_or_default(),
706 architecture: body.architecture.clone(),
707 system_id: body.system_id.clone().unwrap_or_default(),
708 installation_id: body.installation_id.clone().unwrap_or_default(),
709 session_id: body.session_id.clone(),
710 metrics_id: body.metrics_id.clone().unwrap_or_default(),
711 is_staff: body.is_staff,
712 time: time.timestamp_millis(),
713 operation: event.operation,
714 file_extension: event.file_extension.unwrap_or_default(),
715 signed_in: wrapper.signed_in,
716 vim_mode: event.vim_mode,
717 copilot_enabled: event.copilot_enabled,
718 copilot_enabled_for_language: event.copilot_enabled_for_language,
719 country_code: country_code.unwrap_or("XX".to_string()),
720 region_code: "".to_string(),
721 city: "".to_string(),
722 historical_event: false,
723 }
724 }
725}
726
727#[derive(Serialize, Debug, clickhouse::Row)]
728pub struct InlineCompletionEventRow {
729 installation_id: String,
730 session_id: Option<String>,
731 provider: String,
732 suggestion_accepted: bool,
733 app_version: String,
734 file_extension: String,
735 os_name: String,
736 os_version: String,
737 release_channel: String,
738 signed_in: bool,
739 #[serde(serialize_with = "serialize_country_code")]
740 country_code: String,
741 region_code: String,
742 city: String,
743 time: i64,
744 is_staff: Option<bool>,
745 major: Option<i32>,
746 minor: Option<i32>,
747 patch: Option<i32>,
748 checksum_matched: bool,
749}
750
751impl InlineCompletionEventRow {
752 fn from_event(
753 event: InlineCompletionEvent,
754 wrapper: &EventWrapper,
755 body: &EventRequestBody,
756 first_event_at: chrono::DateTime<chrono::Utc>,
757 country_code: Option<String>,
758 checksum_matched: bool,
759 ) -> Self {
760 let semver = body.semver();
761 let time =
762 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
763
764 Self {
765 app_version: body.app_version.clone(),
766 major: semver.map(|v| v.major() as i32),
767 minor: semver.map(|v| v.minor() as i32),
768 patch: semver.map(|v| v.patch() as i32),
769 checksum_matched,
770 release_channel: body.release_channel.clone().unwrap_or_default(),
771 os_name: body.os_name.clone(),
772 os_version: body.os_version.clone().unwrap_or_default(),
773 installation_id: body.installation_id.clone().unwrap_or_default(),
774 session_id: body.session_id.clone(),
775 is_staff: body.is_staff,
776 time: time.timestamp_millis(),
777 file_extension: event.file_extension.unwrap_or_default(),
778 signed_in: wrapper.signed_in,
779 country_code: country_code.unwrap_or("XX".to_string()),
780 region_code: "".to_string(),
781 city: "".to_string(),
782 provider: event.provider,
783 suggestion_accepted: event.suggestion_accepted,
784 }
785 }
786}
787
788#[derive(Serialize, Debug, clickhouse::Row)]
789pub struct CallEventRow {
790 // AppInfoBase
791 app_version: String,
792 major: Option<i32>,
793 minor: Option<i32>,
794 patch: Option<i32>,
795 release_channel: String,
796 os_name: String,
797 os_version: String,
798 checksum_matched: bool,
799
800 // ClientEventBase
801 installation_id: String,
802 session_id: Option<String>,
803 is_staff: Option<bool>,
804 time: i64,
805
806 // CallEventRow
807 operation: String,
808 room_id: Option<u64>,
809 channel_id: Option<u64>,
810}
811
812impl CallEventRow {
813 fn from_event(
814 event: CallEvent,
815 wrapper: &EventWrapper,
816 body: &EventRequestBody,
817 first_event_at: chrono::DateTime<chrono::Utc>,
818 checksum_matched: bool,
819 ) -> Self {
820 let semver = body.semver();
821 let time =
822 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
823
824 Self {
825 app_version: body.app_version.clone(),
826 major: semver.map(|v| v.major() as i32),
827 minor: semver.map(|v| v.minor() as i32),
828 patch: semver.map(|v| v.patch() as i32),
829 checksum_matched,
830 release_channel: body.release_channel.clone().unwrap_or_default(),
831 os_name: body.os_name.clone(),
832 os_version: body.os_version.clone().unwrap_or_default(),
833 installation_id: body.installation_id.clone().unwrap_or_default(),
834 session_id: body.session_id.clone(),
835 is_staff: body.is_staff,
836 time: time.timestamp_millis(),
837 operation: event.operation,
838 room_id: event.room_id,
839 channel_id: event.channel_id,
840 }
841 }
842}
843
844#[derive(Serialize, Debug, clickhouse::Row)]
845pub struct AssistantEventRow {
846 // AppInfoBase
847 app_version: String,
848 major: Option<i32>,
849 minor: Option<i32>,
850 patch: Option<i32>,
851 checksum_matched: bool,
852 release_channel: String,
853 os_name: String,
854 os_version: String,
855
856 // ClientEventBase
857 installation_id: Option<String>,
858 session_id: Option<String>,
859 is_staff: Option<bool>,
860 time: i64,
861
862 // AssistantEventRow
863 conversation_id: String,
864 kind: String,
865 phase: String,
866 model: String,
867 response_latency_in_ms: Option<i64>,
868 error_message: Option<String>,
869}
870
871impl AssistantEventRow {
872 fn from_event(
873 event: AssistantEvent,
874 wrapper: &EventWrapper,
875 body: &EventRequestBody,
876 first_event_at: chrono::DateTime<chrono::Utc>,
877 checksum_matched: bool,
878 ) -> Self {
879 let semver = body.semver();
880 let time =
881 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
882
883 Self {
884 app_version: body.app_version.clone(),
885 major: semver.map(|v| v.major() as i32),
886 minor: semver.map(|v| v.minor() as i32),
887 patch: semver.map(|v| v.patch() as i32),
888 checksum_matched,
889 release_channel: body.release_channel.clone().unwrap_or_default(),
890 os_name: body.os_name.clone(),
891 os_version: body.os_version.clone().unwrap_or_default(),
892 installation_id: body.installation_id.clone(),
893 session_id: body.session_id.clone(),
894 is_staff: body.is_staff,
895 time: time.timestamp_millis(),
896 conversation_id: event.conversation_id.unwrap_or_default(),
897 kind: event.kind.to_string(),
898 phase: event.phase.to_string(),
899 model: event.model,
900 response_latency_in_ms: event
901 .response_latency
902 .map(|latency| latency.as_millis() as i64),
903 error_message: event.error_message,
904 }
905 }
906}
907
908#[derive(Debug, clickhouse::Row, Serialize)]
909pub struct CpuEventRow {
910 installation_id: Option<String>,
911 session_id: Option<String>,
912 is_staff: Option<bool>,
913 usage_as_percentage: f32,
914 core_count: u32,
915 app_version: String,
916 release_channel: String,
917 os_name: String,
918 os_version: String,
919 time: i64,
920 // pub normalized_cpu_usage: f64, MATERIALIZED
921 major: Option<i32>,
922 minor: Option<i32>,
923 patch: Option<i32>,
924 checksum_matched: bool,
925}
926
927impl CpuEventRow {
928 fn from_event(
929 event: CpuEvent,
930 wrapper: &EventWrapper,
931 body: &EventRequestBody,
932 first_event_at: chrono::DateTime<chrono::Utc>,
933 checksum_matched: bool,
934 ) -> Self {
935 let semver = body.semver();
936 let time =
937 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
938
939 Self {
940 app_version: body.app_version.clone(),
941 major: semver.map(|v| v.major() as i32),
942 minor: semver.map(|v| v.minor() as i32),
943 patch: semver.map(|v| v.patch() as i32),
944 checksum_matched,
945 release_channel: body.release_channel.clone().unwrap_or_default(),
946 os_name: body.os_name.clone(),
947 os_version: body.os_version.clone().unwrap_or_default(),
948 installation_id: body.installation_id.clone(),
949 session_id: body.session_id.clone(),
950 is_staff: body.is_staff,
951 time: time.timestamp_millis(),
952 usage_as_percentage: event.usage_as_percentage,
953 core_count: event.core_count,
954 }
955 }
956}
957
958#[derive(Serialize, Debug, clickhouse::Row)]
959pub struct MemoryEventRow {
960 // AppInfoBase
961 app_version: String,
962 major: Option<i32>,
963 minor: Option<i32>,
964 patch: Option<i32>,
965 checksum_matched: bool,
966 release_channel: String,
967 os_name: String,
968 os_version: String,
969
970 // ClientEventBase
971 installation_id: Option<String>,
972 session_id: Option<String>,
973 is_staff: Option<bool>,
974 time: i64,
975
976 // MemoryEventRow
977 memory_in_bytes: u64,
978 virtual_memory_in_bytes: u64,
979}
980
981impl MemoryEventRow {
982 fn from_event(
983 event: MemoryEvent,
984 wrapper: &EventWrapper,
985 body: &EventRequestBody,
986 first_event_at: chrono::DateTime<chrono::Utc>,
987 checksum_matched: bool,
988 ) -> Self {
989 let semver = body.semver();
990 let time =
991 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
992
993 Self {
994 app_version: body.app_version.clone(),
995 major: semver.map(|v| v.major() as i32),
996 minor: semver.map(|v| v.minor() as i32),
997 patch: semver.map(|v| v.patch() as i32),
998 checksum_matched,
999 release_channel: body.release_channel.clone().unwrap_or_default(),
1000 os_name: body.os_name.clone(),
1001 os_version: body.os_version.clone().unwrap_or_default(),
1002 installation_id: body.installation_id.clone(),
1003 session_id: body.session_id.clone(),
1004 is_staff: body.is_staff,
1005 time: time.timestamp_millis(),
1006 memory_in_bytes: event.memory_in_bytes,
1007 virtual_memory_in_bytes: event.virtual_memory_in_bytes,
1008 }
1009 }
1010}
1011
1012#[derive(Serialize, Debug, clickhouse::Row)]
1013pub struct AppEventRow {
1014 // AppInfoBase
1015 app_version: String,
1016 major: Option<i32>,
1017 minor: Option<i32>,
1018 patch: Option<i32>,
1019 checksum_matched: bool,
1020 release_channel: String,
1021 os_name: String,
1022 os_version: String,
1023
1024 // ClientEventBase
1025 installation_id: Option<String>,
1026 session_id: Option<String>,
1027 is_staff: Option<bool>,
1028 time: i64,
1029
1030 // AppEventRow
1031 operation: String,
1032}
1033
1034impl AppEventRow {
1035 fn from_event(
1036 event: AppEvent,
1037 wrapper: &EventWrapper,
1038 body: &EventRequestBody,
1039 first_event_at: chrono::DateTime<chrono::Utc>,
1040 checksum_matched: bool,
1041 ) -> Self {
1042 let semver = body.semver();
1043 let time =
1044 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1045
1046 Self {
1047 app_version: body.app_version.clone(),
1048 major: semver.map(|v| v.major() as i32),
1049 minor: semver.map(|v| v.minor() as i32),
1050 patch: semver.map(|v| v.patch() as i32),
1051 checksum_matched,
1052 release_channel: body.release_channel.clone().unwrap_or_default(),
1053 os_name: body.os_name.clone(),
1054 os_version: body.os_version.clone().unwrap_or_default(),
1055 installation_id: body.installation_id.clone(),
1056 session_id: body.session_id.clone(),
1057 is_staff: body.is_staff,
1058 time: time.timestamp_millis(),
1059 operation: event.operation,
1060 }
1061 }
1062}
1063
1064#[derive(Serialize, Debug, clickhouse::Row)]
1065pub struct SettingEventRow {
1066 // AppInfoBase
1067 app_version: String,
1068 major: Option<i32>,
1069 minor: Option<i32>,
1070 patch: Option<i32>,
1071 checksum_matched: bool,
1072 release_channel: String,
1073 os_name: String,
1074 os_version: String,
1075
1076 // ClientEventBase
1077 installation_id: Option<String>,
1078 session_id: Option<String>,
1079 is_staff: Option<bool>,
1080 time: i64,
1081 // SettingEventRow
1082 setting: String,
1083 value: String,
1084}
1085
1086impl SettingEventRow {
1087 fn from_event(
1088 event: SettingEvent,
1089 wrapper: &EventWrapper,
1090 body: &EventRequestBody,
1091 first_event_at: chrono::DateTime<chrono::Utc>,
1092 checksum_matched: bool,
1093 ) -> Self {
1094 let semver = body.semver();
1095 let time =
1096 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1097
1098 Self {
1099 app_version: body.app_version.clone(),
1100 major: semver.map(|v| v.major() as i32),
1101 minor: semver.map(|v| v.minor() as i32),
1102 checksum_matched,
1103 patch: semver.map(|v| v.patch() as i32),
1104 release_channel: body.release_channel.clone().unwrap_or_default(),
1105 os_name: body.os_name.clone(),
1106 os_version: body.os_version.clone().unwrap_or_default(),
1107 installation_id: body.installation_id.clone(),
1108 session_id: body.session_id.clone(),
1109 is_staff: body.is_staff,
1110 time: time.timestamp_millis(),
1111 setting: event.setting,
1112 value: event.value,
1113 }
1114 }
1115}
1116
1117#[derive(Serialize, Debug, clickhouse::Row)]
1118pub struct ExtensionEventRow {
1119 // AppInfoBase
1120 app_version: String,
1121 major: Option<i32>,
1122 minor: Option<i32>,
1123 patch: Option<i32>,
1124 checksum_matched: bool,
1125 release_channel: String,
1126 os_name: String,
1127 os_version: String,
1128
1129 // ClientEventBase
1130 installation_id: Option<String>,
1131 session_id: Option<String>,
1132 is_staff: Option<bool>,
1133 time: i64,
1134
1135 // ExtensionEventRow
1136 extension_id: Arc<str>,
1137 extension_version: Arc<str>,
1138 dev: bool,
1139 schema_version: Option<i32>,
1140 wasm_api_version: Option<String>,
1141}
1142
1143impl ExtensionEventRow {
1144 fn from_event(
1145 event: ExtensionEvent,
1146 wrapper: &EventWrapper,
1147 body: &EventRequestBody,
1148 extension_metadata: Option<ExtensionMetadata>,
1149 first_event_at: chrono::DateTime<chrono::Utc>,
1150 checksum_matched: bool,
1151 ) -> Self {
1152 let semver = body.semver();
1153 let time =
1154 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1155
1156 Self {
1157 app_version: body.app_version.clone(),
1158 major: semver.map(|v| v.major() as i32),
1159 minor: semver.map(|v| v.minor() as i32),
1160 patch: semver.map(|v| v.patch() as i32),
1161 checksum_matched,
1162 release_channel: body.release_channel.clone().unwrap_or_default(),
1163 os_name: body.os_name.clone(),
1164 os_version: body.os_version.clone().unwrap_or_default(),
1165 installation_id: body.installation_id.clone(),
1166 session_id: body.session_id.clone(),
1167 is_staff: body.is_staff,
1168 time: time.timestamp_millis(),
1169 extension_id: event.extension_id,
1170 extension_version: event.version,
1171 dev: extension_metadata.is_none(),
1172 schema_version: extension_metadata
1173 .as_ref()
1174 .and_then(|metadata| metadata.manifest.schema_version),
1175 wasm_api_version: extension_metadata.as_ref().and_then(|metadata| {
1176 metadata
1177 .manifest
1178 .wasm_api_version
1179 .as_ref()
1180 .map(|version| version.to_string())
1181 }),
1182 }
1183 }
1184}
1185
1186#[derive(Serialize, Debug, clickhouse::Row)]
1187pub struct ReplEventRow {
1188 // AppInfoBase
1189 app_version: String,
1190 major: Option<i32>,
1191 minor: Option<i32>,
1192 patch: Option<i32>,
1193 checksum_matched: bool,
1194 release_channel: String,
1195 os_name: String,
1196 os_version: String,
1197
1198 // ClientEventBase
1199 installation_id: Option<String>,
1200 session_id: Option<String>,
1201 is_staff: Option<bool>,
1202 time: i64,
1203
1204 // ReplEventRow
1205 kernel_language: String,
1206 kernel_status: String,
1207 repl_session_id: String,
1208}
1209
1210impl ReplEventRow {
1211 fn from_event(
1212 event: ReplEvent,
1213 wrapper: &EventWrapper,
1214 body: &EventRequestBody,
1215 first_event_at: chrono::DateTime<chrono::Utc>,
1216 checksum_matched: bool,
1217 ) -> Self {
1218 let semver = body.semver();
1219 let time =
1220 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1221
1222 Self {
1223 app_version: body.app_version.clone(),
1224 major: semver.map(|v| v.major() as i32),
1225 minor: semver.map(|v| v.minor() as i32),
1226 patch: semver.map(|v| v.patch() as i32),
1227 checksum_matched,
1228 release_channel: body.release_channel.clone().unwrap_or_default(),
1229 os_name: body.os_name.clone(),
1230 os_version: body.os_version.clone().unwrap_or_default(),
1231 installation_id: body.installation_id.clone(),
1232 session_id: body.session_id.clone(),
1233 is_staff: body.is_staff,
1234 time: time.timestamp_millis(),
1235 kernel_language: event.kernel_language,
1236 kernel_status: event.kernel_status,
1237 repl_session_id: event.repl_session_id,
1238 }
1239 }
1240}
1241
1242#[derive(Serialize, Debug, clickhouse::Row)]
1243pub struct EditEventRow {
1244 // AppInfoBase
1245 app_version: String,
1246 major: Option<i32>,
1247 minor: Option<i32>,
1248 patch: Option<i32>,
1249 checksum_matched: bool,
1250 release_channel: String,
1251 os_name: String,
1252 os_version: String,
1253
1254 // ClientEventBase
1255 installation_id: Option<String>,
1256 // Note: This column name has a typo in the ClickHouse table.
1257 #[serde(rename = "sesssion_id")]
1258 session_id: Option<String>,
1259 is_staff: Option<bool>,
1260 time: i64,
1261
1262 // EditEventRow
1263 period_start: i64,
1264 period_end: i64,
1265 environment: String,
1266}
1267
1268impl EditEventRow {
1269 fn from_event(
1270 event: EditEvent,
1271 wrapper: &EventWrapper,
1272 body: &EventRequestBody,
1273 first_event_at: chrono::DateTime<chrono::Utc>,
1274 checksum_matched: bool,
1275 ) -> Self {
1276 let semver = body.semver();
1277 let time =
1278 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1279
1280 let period_start = time - chrono::Duration::milliseconds(event.duration);
1281 let period_end = time;
1282
1283 Self {
1284 app_version: body.app_version.clone(),
1285 major: semver.map(|v| v.major() as i32),
1286 minor: semver.map(|v| v.minor() as i32),
1287 patch: semver.map(|v| v.patch() as i32),
1288 checksum_matched,
1289 release_channel: body.release_channel.clone().unwrap_or_default(),
1290 os_name: body.os_name.clone(),
1291 os_version: body.os_version.clone().unwrap_or_default(),
1292 installation_id: body.installation_id.clone(),
1293 session_id: body.session_id.clone(),
1294 is_staff: body.is_staff,
1295 time: time.timestamp_millis(),
1296 period_start: period_start.timestamp_millis(),
1297 period_end: period_end.timestamp_millis(),
1298 environment: event.environment,
1299 }
1300 }
1301}
1302
1303#[derive(Serialize, Debug, clickhouse::Row)]
1304pub struct ActionEventRow {
1305 // AppInfoBase
1306 app_version: String,
1307 major: Option<i32>,
1308 minor: Option<i32>,
1309 patch: Option<i32>,
1310 checksum_matched: bool,
1311 release_channel: String,
1312 os_name: String,
1313 os_version: String,
1314
1315 // ClientEventBase
1316 installation_id: Option<String>,
1317 // Note: This column name has a typo in the ClickHouse table.
1318 #[serde(rename = "sesssion_id")]
1319 session_id: Option<String>,
1320 is_staff: Option<bool>,
1321 time: i64,
1322 // ActionEventRow
1323 source: String,
1324 action: String,
1325}
1326
1327impl ActionEventRow {
1328 fn from_event(
1329 event: ActionEvent,
1330 wrapper: &EventWrapper,
1331 body: &EventRequestBody,
1332 first_event_at: chrono::DateTime<chrono::Utc>,
1333 checksum_matched: bool,
1334 ) -> Self {
1335 let semver = body.semver();
1336 let time =
1337 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1338
1339 Self {
1340 app_version: body.app_version.clone(),
1341 major: semver.map(|v| v.major() as i32),
1342 minor: semver.map(|v| v.minor() as i32),
1343 patch: semver.map(|v| v.patch() as i32),
1344 checksum_matched,
1345 release_channel: body.release_channel.clone().unwrap_or_default(),
1346 os_name: body.os_name.clone(),
1347 os_version: body.os_version.clone().unwrap_or_default(),
1348 installation_id: body.installation_id.clone(),
1349 session_id: body.session_id.clone(),
1350 is_staff: body.is_staff,
1351 time: time.timestamp_millis(),
1352 source: event.source,
1353 action: event.action,
1354 }
1355 }
1356}
1357
1358pub fn calculate_json_checksum(app: Arc<AppState>, json: &impl AsRef<[u8]>) -> Option<Vec<u8>> {
1359 let checksum_seed = app.config.zed_client_checksum_seed.as_ref()?;
1360
1361 let mut summer = Sha256::new();
1362 summer.update(checksum_seed);
1363 summer.update(json);
1364 summer.update(checksum_seed);
1365 Some(summer.finalize().into_iter().collect())
1366}