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 chrono::Duration;
15use rpc::ExtensionMetadata;
16use semantic_version::SemanticVersion;
17use serde::{Deserialize, Serialize, Serializer};
18use serde_json::json;
19use sha2::{Digest, Sha256};
20use std::sync::{Arc, OnceLock};
21use telemetry_events::{
22 ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
23 EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, Panic,
24 ReplEvent, SettingEvent,
25};
26use util::ResultExt;
27use uuid::Uuid;
28
29const CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
30
31pub fn router() -> Router {
32 Router::new()
33 .route("/telemetry/events", post(post_events))
34 .route("/telemetry/crashes", post(post_crash))
35 .route("/telemetry/panics", post(post_panic))
36 .route("/telemetry/hangs", post(post_hang))
37}
38
39pub struct ZedChecksumHeader(Vec<u8>);
40
41impl Header for ZedChecksumHeader {
42 fn name() -> &'static HeaderName {
43 static ZED_CHECKSUM_HEADER: OnceLock<HeaderName> = OnceLock::new();
44 ZED_CHECKSUM_HEADER.get_or_init(|| HeaderName::from_static("x-zed-checksum"))
45 }
46
47 fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
48 where
49 Self: Sized,
50 I: Iterator<Item = &'i axum::http::HeaderValue>,
51 {
52 let checksum = values
53 .next()
54 .ok_or_else(axum::headers::Error::invalid)?
55 .to_str()
56 .map_err(|_| axum::headers::Error::invalid())?;
57
58 let bytes = hex::decode(checksum).map_err(|_| axum::headers::Error::invalid())?;
59 Ok(Self(bytes))
60 }
61
62 fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
63 unimplemented!()
64 }
65}
66
67pub async fn post_crash(
68 Extension(app): Extension<Arc<AppState>>,
69 headers: HeaderMap,
70 body: Bytes,
71) -> Result<()> {
72 let report = IpsFile::parse(&body)?;
73 let version_threshold = SemanticVersion::new(0, 123, 0);
74
75 let bundle_id = &report.header.bundle_id;
76 let app_version = &report.app_version();
77
78 if bundle_id == "dev.zed.Zed-Dev" {
79 log::error!("Crash uploads from {} are ignored.", bundle_id);
80 return Ok(());
81 }
82
83 if app_version.is_none() || app_version.unwrap() < version_threshold {
84 log::error!(
85 "Crash uploads from {} are ignored.",
86 report.header.app_version
87 );
88 return Ok(());
89 }
90 let app_version = app_version.unwrap();
91
92 if let Some(blob_store_client) = app.blob_store_client.as_ref() {
93 let response = blob_store_client
94 .head_object()
95 .bucket(CRASH_REPORTS_BUCKET)
96 .key(report.header.incident_id.clone() + ".ips")
97 .send()
98 .await;
99
100 if response.is_ok() {
101 log::info!("We've already uploaded this crash");
102 return Ok(());
103 }
104
105 blob_store_client
106 .put_object()
107 .bucket(CRASH_REPORTS_BUCKET)
108 .key(report.header.incident_id.clone() + ".ips")
109 .acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
110 .body(ByteStream::from(body.to_vec()))
111 .send()
112 .await
113 .map_err(|e| log::error!("Failed to upload crash: {}", e))
114 .ok();
115 }
116
117 let recent_panic_on: Option<i64> = headers
118 .get("x-zed-panicked-on")
119 .and_then(|h| h.to_str().ok())
120 .and_then(|s| s.parse().ok());
121
122 let installation_id = headers
123 .get("x-zed-installation-id")
124 .and_then(|h| h.to_str().ok())
125 .map(|s| s.to_string())
126 .unwrap_or_default();
127
128 let mut recent_panic = None;
129
130 if let Some(recent_panic_on) = recent_panic_on {
131 let crashed_at = match report.timestamp() {
132 Ok(t) => Some(t),
133 Err(e) => {
134 log::error!("Can't parse {}: {}", report.header.timestamp, e);
135 None
136 }
137 };
138 if crashed_at.is_some_and(|t| (t.timestamp_millis() - recent_panic_on).abs() <= 30000) {
139 recent_panic = headers.get("x-zed-panic").and_then(|h| h.to_str().ok());
140 }
141 }
142
143 let description = report.description(recent_panic);
144 let summary = report.backtrace_summary();
145
146 tracing::error!(
147 service = "client",
148 version = %report.header.app_version,
149 os_version = %report.header.os_version,
150 bundle_id = %report.header.bundle_id,
151 incident_id = %report.header.incident_id,
152 installation_id = %installation_id,
153 description = %description,
154 backtrace = %summary,
155 "crash report"
156 );
157
158 if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
159 let payload = slack::WebhookBody::new(|w| {
160 w.add_section(|s| s.text(slack::Text::markdown(description)))
161 .add_section(|s| {
162 s.add_field(slack::Text::markdown(format!(
163 "*Version:*\n{} ({})",
164 bundle_id, app_version
165 )))
166 .add_field({
167 let hostname = app.config.blob_store_url.clone().unwrap_or_default();
168 let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
169 hostname.strip_prefix("http://").unwrap_or_default()
170 });
171
172 slack::Text::markdown(format!(
173 "*Incident:*\n<https://{}.{}/{}.ips|{}…>",
174 CRASH_REPORTS_BUCKET,
175 hostname,
176 report.header.incident_id,
177 report
178 .header
179 .incident_id
180 .chars()
181 .take(8)
182 .collect::<String>(),
183 ))
184 })
185 })
186 .add_rich_text(|r| r.add_preformatted(|p| p.add_text(summary)))
187 });
188 let payload_json = serde_json::to_string(&payload).map_err(|err| {
189 log::error!("Failed to serialize payload to JSON: {err}");
190 Error::Internal(anyhow!(err))
191 })?;
192
193 reqwest::Client::new()
194 .post(slack_panics_webhook)
195 .header("Content-Type", "application/json")
196 .body(payload_json)
197 .send()
198 .await
199 .map_err(|err| {
200 log::error!("Failed to send payload to Slack: {err}");
201 Error::Internal(anyhow!(err))
202 })?;
203 }
204
205 Ok(())
206}
207
208pub async fn post_hang(
209 Extension(app): Extension<Arc<AppState>>,
210 TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
211 body: Bytes,
212) -> Result<()> {
213 let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
214 return Err(Error::http(
215 StatusCode::INTERNAL_SERVER_ERROR,
216 "events not enabled".into(),
217 ))?;
218 };
219
220 if checksum != expected {
221 return Err(Error::http(
222 StatusCode::BAD_REQUEST,
223 "invalid checksum".into(),
224 ))?;
225 }
226
227 let incident_id = Uuid::new_v4().to_string();
228
229 // dump JSON into S3 so we can get frame offsets if we need to.
230 if let Some(blob_store_client) = app.blob_store_client.as_ref() {
231 blob_store_client
232 .put_object()
233 .bucket(CRASH_REPORTS_BUCKET)
234 .key(incident_id.clone() + ".hang.json")
235 .acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
236 .body(ByteStream::from(body.to_vec()))
237 .send()
238 .await
239 .map_err(|e| log::error!("Failed to upload crash: {}", e))
240 .ok();
241 }
242
243 let report: telemetry_events::HangReport = serde_json::from_slice(&body).map_err(|err| {
244 log::error!("can't parse report json: {err}");
245 Error::Internal(anyhow!(err))
246 })?;
247
248 let mut backtrace = "Possible hang detected on main thread:".to_string();
249 let unknown = "<unknown>".to_string();
250 for frame in report.backtrace.iter() {
251 backtrace.push_str(&format!("\n{}", frame.symbols.first().unwrap_or(&unknown)));
252 }
253
254 tracing::error!(
255 service = "client",
256 version = %report.app_version.unwrap_or_default().to_string(),
257 os_name = %report.os_name,
258 os_version = report.os_version.unwrap_or_default().to_string(),
259 incident_id = %incident_id,
260 installation_id = %report.installation_id.unwrap_or_default(),
261 backtrace = %backtrace,
262 "hang report");
263
264 Ok(())
265}
266
267pub async fn post_panic(
268 Extension(app): Extension<Arc<AppState>>,
269 TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
270 body: Bytes,
271) -> Result<()> {
272 let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
273 return Err(Error::http(
274 StatusCode::INTERNAL_SERVER_ERROR,
275 "events not enabled".into(),
276 ))?;
277 };
278
279 if checksum != expected {
280 return Err(Error::http(
281 StatusCode::BAD_REQUEST,
282 "invalid checksum".into(),
283 ))?;
284 }
285
286 let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
287 .map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
288 let panic = report.panic;
289
290 if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
291 return Err(Error::http(
292 StatusCode::BAD_REQUEST,
293 "invalid os version".into(),
294 ))?;
295 }
296
297 tracing::error!(
298 service = "client",
299 version = %panic.app_version,
300 os_name = %panic.os_name,
301 os_version = %panic.os_version.clone().unwrap_or_default(),
302 installation_id = %panic.installation_id.clone().unwrap_or_default(),
303 description = %panic.payload,
304 backtrace = %panic.backtrace.join("\n"),
305 "panic report"
306 );
307
308 let backtrace = if panic.backtrace.len() > 25 {
309 let total = panic.backtrace.len();
310 format!(
311 "{}\n and {} more",
312 panic
313 .backtrace
314 .iter()
315 .take(20)
316 .cloned()
317 .collect::<Vec<_>>()
318 .join("\n"),
319 total - 20
320 )
321 } else {
322 panic.backtrace.join("\n")
323 };
324
325 if !report_to_slack(&panic) {
326 return Ok(());
327 }
328
329 let backtrace_with_summary = panic.payload + "\n" + &backtrace;
330
331 if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
332 let payload = slack::WebhookBody::new(|w| {
333 w.add_section(|s| s.text(slack::Text::markdown("Panic request".to_string())))
334 .add_section(|s| {
335 s.add_field(slack::Text::markdown(format!(
336 "*Version:*\n {} ",
337 panic.app_version
338 )))
339 .add_field({
340 slack::Text::markdown(format!(
341 "*OS:*\n{} {}",
342 panic.os_name,
343 panic.os_version.unwrap_or_default()
344 ))
345 })
346 })
347 .add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace_with_summary)))
348 });
349 let payload_json = serde_json::to_string(&payload).map_err(|err| {
350 log::error!("Failed to serialize payload to JSON: {err}");
351 Error::Internal(anyhow!(err))
352 })?;
353
354 reqwest::Client::new()
355 .post(slack_panics_webhook)
356 .header("Content-Type", "application/json")
357 .body(payload_json)
358 .send()
359 .await
360 .map_err(|err| {
361 log::error!("Failed to send payload to Slack: {err}");
362 Error::Internal(anyhow!(err))
363 })?;
364 }
365
366 Ok(())
367}
368
369fn report_to_slack(panic: &Panic) -> bool {
370 if panic.payload.contains("ERROR_SURFACE_LOST_KHR") {
371 return false;
372 }
373
374 if panic.payload.contains("ERROR_INITIALIZATION_FAILED") {
375 return false;
376 }
377
378 if panic
379 .payload
380 .contains("GPU has crashed, and no debug information is available")
381 {
382 return false;
383 }
384
385 true
386}
387
388pub async fn post_events(
389 Extension(app): Extension<Arc<AppState>>,
390 TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
391 country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
392 body: Bytes,
393) -> Result<()> {
394 let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
395 return Err(Error::http(
396 StatusCode::INTERNAL_SERVER_ERROR,
397 "events not enabled".into(),
398 ))?;
399 };
400
401 let checksum_matched = checksum == expected;
402
403 let request_body: telemetry_events::EventRequestBody =
404 serde_json::from_slice(&body).map_err(|err| {
405 log::error!("can't parse event json: {err}");
406 Error::Internal(anyhow!(err))
407 })?;
408
409 let mut to_upload = ToUpload::default();
410 let Some(last_event) = request_body.events.last() else {
411 return Err(Error::http(StatusCode::BAD_REQUEST, "no events".into()))?;
412 };
413 let country_code = country_code_header.map(|h| h.to_string());
414
415 let first_event_at = chrono::Utc::now()
416 - chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
417
418 if let Some(kinesis_client) = app.kinesis_client.clone() {
419 if let Some(stream) = app.config.kinesis_stream.clone() {
420 let mut request = kinesis_client.put_records().stream_name(stream);
421 for row in for_snowflake(request_body.clone(), first_event_at, country_code.clone()) {
422 if let Some(data) = serde_json::to_vec(&row).log_err() {
423 request = request.records(
424 aws_sdk_kinesis::types::PutRecordsRequestEntry::builder()
425 .partition_key(request_body.system_id.clone().unwrap_or_default())
426 .data(data.into())
427 .build()
428 .unwrap(),
429 );
430 }
431 }
432 request.send().await.log_err();
433 }
434 };
435
436 let Some(clickhouse_client) = app.clickhouse_client.clone() else {
437 Err(Error::http(
438 StatusCode::NOT_IMPLEMENTED,
439 "not supported".into(),
440 ))?
441 };
442
443 let first_event_at = chrono::Utc::now()
444 - chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
445
446 for wrapper in &request_body.events {
447 match &wrapper.event {
448 Event::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event(
449 event.clone(),
450 wrapper,
451 &request_body,
452 first_event_at,
453 country_code.clone(),
454 checksum_matched,
455 )),
456 Event::InlineCompletion(event) => {
457 to_upload
458 .inline_completion_events
459 .push(InlineCompletionEventRow::from_event(
460 event.clone(),
461 wrapper,
462 &request_body,
463 first_event_at,
464 country_code.clone(),
465 checksum_matched,
466 ))
467 }
468 Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
469 event.clone(),
470 wrapper,
471 &request_body,
472 first_event_at,
473 checksum_matched,
474 )),
475 Event::Assistant(event) => {
476 to_upload
477 .assistant_events
478 .push(AssistantEventRow::from_event(
479 event.clone(),
480 wrapper,
481 &request_body,
482 first_event_at,
483 checksum_matched,
484 ))
485 }
486 Event::Cpu(_) | Event::Memory(_) => continue,
487 Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
488 event.clone(),
489 wrapper,
490 &request_body,
491 first_event_at,
492 checksum_matched,
493 )),
494 Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event(
495 event.clone(),
496 wrapper,
497 &request_body,
498 first_event_at,
499 checksum_matched,
500 )),
501 Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
502 event.clone(),
503 wrapper,
504 &request_body,
505 first_event_at,
506 checksum_matched,
507 )),
508 Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
509 event.clone(),
510 wrapper,
511 &request_body,
512 first_event_at,
513 checksum_matched,
514 )),
515 Event::Extension(event) => {
516 let metadata = app
517 .db
518 .get_extension_version(&event.extension_id, &event.version)
519 .await?;
520 to_upload
521 .extension_events
522 .push(ExtensionEventRow::from_event(
523 event.clone(),
524 wrapper,
525 &request_body,
526 metadata,
527 first_event_at,
528 checksum_matched,
529 ))
530 }
531 Event::Repl(event) => to_upload.repl_events.push(ReplEventRow::from_event(
532 event.clone(),
533 wrapper,
534 &request_body,
535 first_event_at,
536 checksum_matched,
537 )),
538 }
539 }
540
541 to_upload
542 .upload(&clickhouse_client)
543 .await
544 .map_err(|err| Error::Internal(anyhow!(err)))?;
545
546 Ok(())
547}
548
549#[derive(Default)]
550struct ToUpload {
551 editor_events: Vec<EditorEventRow>,
552 inline_completion_events: Vec<InlineCompletionEventRow>,
553 assistant_events: Vec<AssistantEventRow>,
554 call_events: Vec<CallEventRow>,
555 cpu_events: Vec<CpuEventRow>,
556 memory_events: Vec<MemoryEventRow>,
557 app_events: Vec<AppEventRow>,
558 setting_events: Vec<SettingEventRow>,
559 extension_events: Vec<ExtensionEventRow>,
560 edit_events: Vec<EditEventRow>,
561 action_events: Vec<ActionEventRow>,
562 repl_events: Vec<ReplEventRow>,
563}
564
565impl ToUpload {
566 pub async fn upload(&self, clickhouse_client: &clickhouse::Client) -> anyhow::Result<()> {
567 const EDITOR_EVENTS_TABLE: &str = "editor_events";
568 write_to_table(EDITOR_EVENTS_TABLE, &self.editor_events, clickhouse_client)
569 .await
570 .with_context(|| format!("failed to upload to table '{EDITOR_EVENTS_TABLE}'"))?;
571
572 const INLINE_COMPLETION_EVENTS_TABLE: &str = "inline_completion_events";
573 write_to_table(
574 INLINE_COMPLETION_EVENTS_TABLE,
575 &self.inline_completion_events,
576 clickhouse_client,
577 )
578 .await
579 .with_context(|| format!("failed to upload to table '{INLINE_COMPLETION_EVENTS_TABLE}'"))?;
580
581 const ASSISTANT_EVENTS_TABLE: &str = "assistant_events";
582 write_to_table(
583 ASSISTANT_EVENTS_TABLE,
584 &self.assistant_events,
585 clickhouse_client,
586 )
587 .await
588 .with_context(|| format!("failed to upload to table '{ASSISTANT_EVENTS_TABLE}'"))?;
589
590 const CALL_EVENTS_TABLE: &str = "call_events";
591 write_to_table(CALL_EVENTS_TABLE, &self.call_events, clickhouse_client)
592 .await
593 .with_context(|| format!("failed to upload to table '{CALL_EVENTS_TABLE}'"))?;
594
595 const CPU_EVENTS_TABLE: &str = "cpu_events";
596 write_to_table(CPU_EVENTS_TABLE, &self.cpu_events, clickhouse_client)
597 .await
598 .with_context(|| format!("failed to upload to table '{CPU_EVENTS_TABLE}'"))?;
599
600 const MEMORY_EVENTS_TABLE: &str = "memory_events";
601 write_to_table(MEMORY_EVENTS_TABLE, &self.memory_events, clickhouse_client)
602 .await
603 .with_context(|| format!("failed to upload to table '{MEMORY_EVENTS_TABLE}'"))?;
604
605 const APP_EVENTS_TABLE: &str = "app_events";
606 write_to_table(APP_EVENTS_TABLE, &self.app_events, clickhouse_client)
607 .await
608 .with_context(|| format!("failed to upload to table '{APP_EVENTS_TABLE}'"))?;
609
610 const SETTING_EVENTS_TABLE: &str = "setting_events";
611 write_to_table(
612 SETTING_EVENTS_TABLE,
613 &self.setting_events,
614 clickhouse_client,
615 )
616 .await
617 .with_context(|| format!("failed to upload to table '{SETTING_EVENTS_TABLE}'"))?;
618
619 const EXTENSION_EVENTS_TABLE: &str = "extension_events";
620 write_to_table(
621 EXTENSION_EVENTS_TABLE,
622 &self.extension_events,
623 clickhouse_client,
624 )
625 .await
626 .with_context(|| format!("failed to upload to table '{EXTENSION_EVENTS_TABLE}'"))?;
627
628 const EDIT_EVENTS_TABLE: &str = "edit_events";
629 write_to_table(EDIT_EVENTS_TABLE, &self.edit_events, clickhouse_client)
630 .await
631 .with_context(|| format!("failed to upload to table '{EDIT_EVENTS_TABLE}'"))?;
632
633 const ACTION_EVENTS_TABLE: &str = "action_events";
634 write_to_table(ACTION_EVENTS_TABLE, &self.action_events, clickhouse_client)
635 .await
636 .with_context(|| format!("failed to upload to table '{ACTION_EVENTS_TABLE}'"))?;
637
638 const REPL_EVENTS_TABLE: &str = "repl_events";
639 write_to_table(REPL_EVENTS_TABLE, &self.repl_events, clickhouse_client)
640 .await
641 .with_context(|| format!("failed to upload to table '{REPL_EVENTS_TABLE}'"))?;
642
643 Ok(())
644 }
645}
646
647pub fn serialize_country_code<S>(country_code: &str, serializer: S) -> Result<S::Ok, S::Error>
648where
649 S: Serializer,
650{
651 if country_code.len() != 2 {
652 use serde::ser::Error;
653 return Err(S::Error::custom(
654 "country_code must be exactly 2 characters",
655 ));
656 }
657
658 let country_code = country_code.as_bytes();
659
660 serializer.serialize_u16(((country_code[1] as u16) << 8) + country_code[0] as u16)
661}
662
663#[derive(Serialize, Debug, clickhouse::Row)]
664pub struct EditorEventRow {
665 system_id: String,
666 installation_id: String,
667 session_id: Option<String>,
668 metrics_id: String,
669 operation: String,
670 app_version: String,
671 file_extension: String,
672 os_name: String,
673 os_version: String,
674 release_channel: String,
675 signed_in: bool,
676 vim_mode: bool,
677 #[serde(serialize_with = "serialize_country_code")]
678 country_code: String,
679 region_code: String,
680 city: String,
681 time: i64,
682 copilot_enabled: bool,
683 copilot_enabled_for_language: bool,
684 architecture: String,
685 is_staff: Option<bool>,
686 major: Option<i32>,
687 minor: Option<i32>,
688 patch: Option<i32>,
689 checksum_matched: bool,
690 is_via_ssh: bool,
691}
692
693impl EditorEventRow {
694 fn from_event(
695 event: EditorEvent,
696 wrapper: &EventWrapper,
697 body: &EventRequestBody,
698 first_event_at: chrono::DateTime<chrono::Utc>,
699 country_code: Option<String>,
700 checksum_matched: bool,
701 ) -> Self {
702 let semver = body.semver();
703 let time =
704 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
705
706 Self {
707 app_version: body.app_version.clone(),
708 major: semver.map(|v| v.major() as i32),
709 minor: semver.map(|v| v.minor() as i32),
710 patch: semver.map(|v| v.patch() as i32),
711 checksum_matched,
712 release_channel: body.release_channel.clone().unwrap_or_default(),
713 os_name: body.os_name.clone(),
714 os_version: body.os_version.clone().unwrap_or_default(),
715 architecture: body.architecture.clone(),
716 system_id: body.system_id.clone().unwrap_or_default(),
717 installation_id: body.installation_id.clone().unwrap_or_default(),
718 session_id: body.session_id.clone(),
719 metrics_id: body.metrics_id.clone().unwrap_or_default(),
720 is_staff: body.is_staff,
721 time: time.timestamp_millis(),
722 operation: event.operation,
723 file_extension: event.file_extension.unwrap_or_default(),
724 signed_in: wrapper.signed_in,
725 vim_mode: event.vim_mode,
726 copilot_enabled: event.copilot_enabled,
727 copilot_enabled_for_language: event.copilot_enabled_for_language,
728 country_code: country_code.unwrap_or("XX".to_string()),
729 region_code: "".to_string(),
730 city: "".to_string(),
731 is_via_ssh: event.is_via_ssh,
732 }
733 }
734}
735
736#[derive(Serialize, Debug, clickhouse::Row)]
737pub struct InlineCompletionEventRow {
738 installation_id: String,
739 session_id: Option<String>,
740 provider: String,
741 suggestion_accepted: bool,
742 app_version: String,
743 file_extension: String,
744 os_name: String,
745 os_version: String,
746 release_channel: String,
747 signed_in: bool,
748 #[serde(serialize_with = "serialize_country_code")]
749 country_code: String,
750 region_code: String,
751 city: String,
752 time: i64,
753 is_staff: Option<bool>,
754 major: Option<i32>,
755 minor: Option<i32>,
756 patch: Option<i32>,
757 checksum_matched: bool,
758}
759
760impl InlineCompletionEventRow {
761 fn from_event(
762 event: InlineCompletionEvent,
763 wrapper: &EventWrapper,
764 body: &EventRequestBody,
765 first_event_at: chrono::DateTime<chrono::Utc>,
766 country_code: Option<String>,
767 checksum_matched: bool,
768 ) -> Self {
769 let semver = body.semver();
770 let time =
771 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
772
773 Self {
774 app_version: body.app_version.clone(),
775 major: semver.map(|v| v.major() as i32),
776 minor: semver.map(|v| v.minor() as i32),
777 patch: semver.map(|v| v.patch() as i32),
778 checksum_matched,
779 release_channel: body.release_channel.clone().unwrap_or_default(),
780 os_name: body.os_name.clone(),
781 os_version: body.os_version.clone().unwrap_or_default(),
782 installation_id: body.installation_id.clone().unwrap_or_default(),
783 session_id: body.session_id.clone(),
784 is_staff: body.is_staff,
785 time: time.timestamp_millis(),
786 file_extension: event.file_extension.unwrap_or_default(),
787 signed_in: wrapper.signed_in,
788 country_code: country_code.unwrap_or("XX".to_string()),
789 region_code: "".to_string(),
790 city: "".to_string(),
791 provider: event.provider,
792 suggestion_accepted: event.suggestion_accepted,
793 }
794 }
795}
796
797#[derive(Serialize, Debug, clickhouse::Row)]
798pub struct CallEventRow {
799 // AppInfoBase
800 app_version: String,
801 major: Option<i32>,
802 minor: Option<i32>,
803 patch: Option<i32>,
804 release_channel: String,
805 os_name: String,
806 os_version: String,
807 checksum_matched: bool,
808
809 // ClientEventBase
810 installation_id: String,
811 session_id: Option<String>,
812 is_staff: Option<bool>,
813 time: i64,
814
815 // CallEventRow
816 operation: String,
817 room_id: Option<u64>,
818 channel_id: Option<u64>,
819}
820
821impl CallEventRow {
822 fn from_event(
823 event: CallEvent,
824 wrapper: &EventWrapper,
825 body: &EventRequestBody,
826 first_event_at: chrono::DateTime<chrono::Utc>,
827 checksum_matched: bool,
828 ) -> Self {
829 let semver = body.semver();
830 let time =
831 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
832
833 Self {
834 app_version: body.app_version.clone(),
835 major: semver.map(|v| v.major() as i32),
836 minor: semver.map(|v| v.minor() as i32),
837 patch: semver.map(|v| v.patch() as i32),
838 checksum_matched,
839 release_channel: body.release_channel.clone().unwrap_or_default(),
840 os_name: body.os_name.clone(),
841 os_version: body.os_version.clone().unwrap_or_default(),
842 installation_id: body.installation_id.clone().unwrap_or_default(),
843 session_id: body.session_id.clone(),
844 is_staff: body.is_staff,
845 time: time.timestamp_millis(),
846 operation: event.operation,
847 room_id: event.room_id,
848 channel_id: event.channel_id,
849 }
850 }
851}
852
853#[derive(Serialize, Debug, clickhouse::Row)]
854pub struct AssistantEventRow {
855 // AppInfoBase
856 app_version: String,
857 major: Option<i32>,
858 minor: Option<i32>,
859 patch: Option<i32>,
860 checksum_matched: bool,
861 release_channel: String,
862 os_name: String,
863 os_version: String,
864
865 // ClientEventBase
866 installation_id: Option<String>,
867 session_id: Option<String>,
868 is_staff: Option<bool>,
869 time: i64,
870
871 // AssistantEventRow
872 conversation_id: String,
873 kind: String,
874 phase: String,
875 model: String,
876 response_latency_in_ms: Option<i64>,
877 error_message: Option<String>,
878}
879
880impl AssistantEventRow {
881 fn from_event(
882 event: AssistantEvent,
883 wrapper: &EventWrapper,
884 body: &EventRequestBody,
885 first_event_at: chrono::DateTime<chrono::Utc>,
886 checksum_matched: bool,
887 ) -> Self {
888 let semver = body.semver();
889 let time =
890 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
891
892 Self {
893 app_version: body.app_version.clone(),
894 major: semver.map(|v| v.major() as i32),
895 minor: semver.map(|v| v.minor() as i32),
896 patch: semver.map(|v| v.patch() as i32),
897 checksum_matched,
898 release_channel: body.release_channel.clone().unwrap_or_default(),
899 os_name: body.os_name.clone(),
900 os_version: body.os_version.clone().unwrap_or_default(),
901 installation_id: body.installation_id.clone(),
902 session_id: body.session_id.clone(),
903 is_staff: body.is_staff,
904 time: time.timestamp_millis(),
905 conversation_id: event.conversation_id.unwrap_or_default(),
906 kind: event.kind.to_string(),
907 phase: event.phase.to_string(),
908 model: event.model,
909 response_latency_in_ms: event
910 .response_latency
911 .map(|latency| latency.as_millis() as i64),
912 error_message: event.error_message,
913 }
914 }
915}
916
917#[derive(Debug, clickhouse::Row, Serialize)]
918pub struct CpuEventRow {
919 installation_id: Option<String>,
920 session_id: Option<String>,
921 is_staff: Option<bool>,
922 usage_as_percentage: f32,
923 core_count: u32,
924 app_version: String,
925 release_channel: String,
926 os_name: String,
927 os_version: String,
928 time: i64,
929 // pub normalized_cpu_usage: f64, MATERIALIZED
930 major: Option<i32>,
931 minor: Option<i32>,
932 patch: Option<i32>,
933 checksum_matched: bool,
934}
935
936impl CpuEventRow {
937 #[allow(unused)]
938 fn from_event(
939 event: CpuEvent,
940 wrapper: &EventWrapper,
941 body: &EventRequestBody,
942 first_event_at: chrono::DateTime<chrono::Utc>,
943 checksum_matched: bool,
944 ) -> Self {
945 let semver = body.semver();
946 let time =
947 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
948
949 Self {
950 app_version: body.app_version.clone(),
951 major: semver.map(|v| v.major() as i32),
952 minor: semver.map(|v| v.minor() as i32),
953 patch: semver.map(|v| v.patch() as i32),
954 checksum_matched,
955 release_channel: body.release_channel.clone().unwrap_or_default(),
956 os_name: body.os_name.clone(),
957 os_version: body.os_version.clone().unwrap_or_default(),
958 installation_id: body.installation_id.clone(),
959 session_id: body.session_id.clone(),
960 is_staff: body.is_staff,
961 time: time.timestamp_millis(),
962 usage_as_percentage: event.usage_as_percentage,
963 core_count: event.core_count,
964 }
965 }
966}
967
968#[derive(Serialize, Debug, clickhouse::Row)]
969pub struct MemoryEventRow {
970 // AppInfoBase
971 app_version: String,
972 major: Option<i32>,
973 minor: Option<i32>,
974 patch: Option<i32>,
975 checksum_matched: bool,
976 release_channel: String,
977 os_name: String,
978 os_version: String,
979
980 // ClientEventBase
981 installation_id: Option<String>,
982 session_id: Option<String>,
983 is_staff: Option<bool>,
984 time: i64,
985
986 // MemoryEventRow
987 memory_in_bytes: u64,
988 virtual_memory_in_bytes: u64,
989}
990
991impl MemoryEventRow {
992 #[allow(unused)]
993 fn from_event(
994 event: MemoryEvent,
995 wrapper: &EventWrapper,
996 body: &EventRequestBody,
997 first_event_at: chrono::DateTime<chrono::Utc>,
998 checksum_matched: bool,
999 ) -> Self {
1000 let semver = body.semver();
1001 let time =
1002 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1003
1004 Self {
1005 app_version: body.app_version.clone(),
1006 major: semver.map(|v| v.major() as i32),
1007 minor: semver.map(|v| v.minor() as i32),
1008 patch: semver.map(|v| v.patch() as i32),
1009 checksum_matched,
1010 release_channel: body.release_channel.clone().unwrap_or_default(),
1011 os_name: body.os_name.clone(),
1012 os_version: body.os_version.clone().unwrap_or_default(),
1013 installation_id: body.installation_id.clone(),
1014 session_id: body.session_id.clone(),
1015 is_staff: body.is_staff,
1016 time: time.timestamp_millis(),
1017 memory_in_bytes: event.memory_in_bytes,
1018 virtual_memory_in_bytes: event.virtual_memory_in_bytes,
1019 }
1020 }
1021}
1022
1023#[derive(Serialize, Debug, clickhouse::Row)]
1024pub struct AppEventRow {
1025 // AppInfoBase
1026 app_version: String,
1027 major: Option<i32>,
1028 minor: Option<i32>,
1029 patch: Option<i32>,
1030 checksum_matched: bool,
1031 release_channel: String,
1032 os_name: String,
1033 os_version: String,
1034
1035 // ClientEventBase
1036 installation_id: Option<String>,
1037 session_id: Option<String>,
1038 is_staff: Option<bool>,
1039 time: i64,
1040
1041 // AppEventRow
1042 operation: String,
1043}
1044
1045impl AppEventRow {
1046 fn from_event(
1047 event: AppEvent,
1048 wrapper: &EventWrapper,
1049 body: &EventRequestBody,
1050 first_event_at: chrono::DateTime<chrono::Utc>,
1051 checksum_matched: bool,
1052 ) -> Self {
1053 let semver = body.semver();
1054 let time =
1055 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1056
1057 Self {
1058 app_version: body.app_version.clone(),
1059 major: semver.map(|v| v.major() as i32),
1060 minor: semver.map(|v| v.minor() as i32),
1061 patch: semver.map(|v| v.patch() as i32),
1062 checksum_matched,
1063 release_channel: body.release_channel.clone().unwrap_or_default(),
1064 os_name: body.os_name.clone(),
1065 os_version: body.os_version.clone().unwrap_or_default(),
1066 installation_id: body.installation_id.clone(),
1067 session_id: body.session_id.clone(),
1068 is_staff: body.is_staff,
1069 time: time.timestamp_millis(),
1070 operation: event.operation,
1071 }
1072 }
1073}
1074
1075#[derive(Serialize, Debug, clickhouse::Row)]
1076pub struct SettingEventRow {
1077 // AppInfoBase
1078 app_version: String,
1079 major: Option<i32>,
1080 minor: Option<i32>,
1081 patch: Option<i32>,
1082 checksum_matched: bool,
1083 release_channel: String,
1084 os_name: String,
1085 os_version: String,
1086
1087 // ClientEventBase
1088 installation_id: Option<String>,
1089 session_id: Option<String>,
1090 is_staff: Option<bool>,
1091 time: i64,
1092 // SettingEventRow
1093 setting: String,
1094 value: String,
1095}
1096
1097impl SettingEventRow {
1098 fn from_event(
1099 event: SettingEvent,
1100 wrapper: &EventWrapper,
1101 body: &EventRequestBody,
1102 first_event_at: chrono::DateTime<chrono::Utc>,
1103 checksum_matched: bool,
1104 ) -> Self {
1105 let semver = body.semver();
1106 let time =
1107 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1108
1109 Self {
1110 app_version: body.app_version.clone(),
1111 major: semver.map(|v| v.major() as i32),
1112 minor: semver.map(|v| v.minor() as i32),
1113 checksum_matched,
1114 patch: semver.map(|v| v.patch() as i32),
1115 release_channel: body.release_channel.clone().unwrap_or_default(),
1116 os_name: body.os_name.clone(),
1117 os_version: body.os_version.clone().unwrap_or_default(),
1118 installation_id: body.installation_id.clone(),
1119 session_id: body.session_id.clone(),
1120 is_staff: body.is_staff,
1121 time: time.timestamp_millis(),
1122 setting: event.setting,
1123 value: event.value,
1124 }
1125 }
1126}
1127
1128#[derive(Serialize, Debug, clickhouse::Row)]
1129pub struct ExtensionEventRow {
1130 // AppInfoBase
1131 app_version: String,
1132 major: Option<i32>,
1133 minor: Option<i32>,
1134 patch: Option<i32>,
1135 checksum_matched: bool,
1136 release_channel: String,
1137 os_name: String,
1138 os_version: String,
1139
1140 // ClientEventBase
1141 installation_id: Option<String>,
1142 session_id: Option<String>,
1143 is_staff: Option<bool>,
1144 time: i64,
1145
1146 // ExtensionEventRow
1147 extension_id: Arc<str>,
1148 extension_version: Arc<str>,
1149 dev: bool,
1150 schema_version: Option<i32>,
1151 wasm_api_version: Option<String>,
1152}
1153
1154impl ExtensionEventRow {
1155 fn from_event(
1156 event: ExtensionEvent,
1157 wrapper: &EventWrapper,
1158 body: &EventRequestBody,
1159 extension_metadata: Option<ExtensionMetadata>,
1160 first_event_at: chrono::DateTime<chrono::Utc>,
1161 checksum_matched: bool,
1162 ) -> Self {
1163 let semver = body.semver();
1164 let time =
1165 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1166
1167 Self {
1168 app_version: body.app_version.clone(),
1169 major: semver.map(|v| v.major() as i32),
1170 minor: semver.map(|v| v.minor() as i32),
1171 patch: semver.map(|v| v.patch() as i32),
1172 checksum_matched,
1173 release_channel: body.release_channel.clone().unwrap_or_default(),
1174 os_name: body.os_name.clone(),
1175 os_version: body.os_version.clone().unwrap_or_default(),
1176 installation_id: body.installation_id.clone(),
1177 session_id: body.session_id.clone(),
1178 is_staff: body.is_staff,
1179 time: time.timestamp_millis(),
1180 extension_id: event.extension_id,
1181 extension_version: event.version,
1182 dev: extension_metadata.is_none(),
1183 schema_version: extension_metadata
1184 .as_ref()
1185 .and_then(|metadata| metadata.manifest.schema_version),
1186 wasm_api_version: extension_metadata.as_ref().and_then(|metadata| {
1187 metadata
1188 .manifest
1189 .wasm_api_version
1190 .as_ref()
1191 .map(|version| version.to_string())
1192 }),
1193 }
1194 }
1195}
1196
1197#[derive(Serialize, Debug, clickhouse::Row)]
1198pub struct ReplEventRow {
1199 // AppInfoBase
1200 app_version: String,
1201 major: Option<i32>,
1202 minor: Option<i32>,
1203 patch: Option<i32>,
1204 checksum_matched: bool,
1205 release_channel: String,
1206 os_name: String,
1207 os_version: String,
1208
1209 // ClientEventBase
1210 installation_id: Option<String>,
1211 session_id: Option<String>,
1212 is_staff: Option<bool>,
1213 time: i64,
1214
1215 // ReplEventRow
1216 kernel_language: String,
1217 kernel_status: String,
1218 repl_session_id: String,
1219}
1220
1221impl ReplEventRow {
1222 fn from_event(
1223 event: ReplEvent,
1224 wrapper: &EventWrapper,
1225 body: &EventRequestBody,
1226 first_event_at: chrono::DateTime<chrono::Utc>,
1227 checksum_matched: bool,
1228 ) -> Self {
1229 let semver = body.semver();
1230 let time =
1231 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1232
1233 Self {
1234 app_version: body.app_version.clone(),
1235 major: semver.map(|v| v.major() as i32),
1236 minor: semver.map(|v| v.minor() as i32),
1237 patch: semver.map(|v| v.patch() as i32),
1238 checksum_matched,
1239 release_channel: body.release_channel.clone().unwrap_or_default(),
1240 os_name: body.os_name.clone(),
1241 os_version: body.os_version.clone().unwrap_or_default(),
1242 installation_id: body.installation_id.clone(),
1243 session_id: body.session_id.clone(),
1244 is_staff: body.is_staff,
1245 time: time.timestamp_millis(),
1246 kernel_language: event.kernel_language,
1247 kernel_status: event.kernel_status,
1248 repl_session_id: event.repl_session_id,
1249 }
1250 }
1251}
1252
1253#[derive(Serialize, Debug, clickhouse::Row)]
1254pub struct EditEventRow {
1255 // AppInfoBase
1256 app_version: String,
1257 major: Option<i32>,
1258 minor: Option<i32>,
1259 patch: Option<i32>,
1260 checksum_matched: bool,
1261 release_channel: String,
1262 os_name: String,
1263 os_version: String,
1264
1265 // ClientEventBase
1266 installation_id: Option<String>,
1267 // Note: This column name has a typo in the ClickHouse table.
1268 #[serde(rename = "sesssion_id")]
1269 session_id: Option<String>,
1270 is_staff: Option<bool>,
1271 time: i64,
1272
1273 // EditEventRow
1274 period_start: i64,
1275 period_end: i64,
1276 environment: String,
1277 is_via_ssh: bool,
1278}
1279
1280impl EditEventRow {
1281 fn from_event(
1282 event: EditEvent,
1283 wrapper: &EventWrapper,
1284 body: &EventRequestBody,
1285 first_event_at: chrono::DateTime<chrono::Utc>,
1286 checksum_matched: bool,
1287 ) -> Self {
1288 let semver = body.semver();
1289 let time =
1290 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1291
1292 let period_start = time - chrono::Duration::milliseconds(event.duration);
1293 let period_end = time;
1294
1295 Self {
1296 app_version: body.app_version.clone(),
1297 major: semver.map(|v| v.major() as i32),
1298 minor: semver.map(|v| v.minor() as i32),
1299 patch: semver.map(|v| v.patch() as i32),
1300 checksum_matched,
1301 release_channel: body.release_channel.clone().unwrap_or_default(),
1302 os_name: body.os_name.clone(),
1303 os_version: body.os_version.clone().unwrap_or_default(),
1304 installation_id: body.installation_id.clone(),
1305 session_id: body.session_id.clone(),
1306 is_staff: body.is_staff,
1307 time: time.timestamp_millis(),
1308 period_start: period_start.timestamp_millis(),
1309 period_end: period_end.timestamp_millis(),
1310 environment: event.environment,
1311 is_via_ssh: event.is_via_ssh,
1312 }
1313 }
1314}
1315
1316#[derive(Serialize, Debug, clickhouse::Row)]
1317pub struct ActionEventRow {
1318 // AppInfoBase
1319 app_version: String,
1320 major: Option<i32>,
1321 minor: Option<i32>,
1322 patch: Option<i32>,
1323 checksum_matched: bool,
1324 release_channel: String,
1325 os_name: String,
1326 os_version: String,
1327
1328 // ClientEventBase
1329 installation_id: Option<String>,
1330 // Note: This column name has a typo in the ClickHouse table.
1331 #[serde(rename = "sesssion_id")]
1332 session_id: Option<String>,
1333 is_staff: Option<bool>,
1334 time: i64,
1335 // ActionEventRow
1336 source: String,
1337 action: String,
1338}
1339
1340impl ActionEventRow {
1341 fn from_event(
1342 event: ActionEvent,
1343 wrapper: &EventWrapper,
1344 body: &EventRequestBody,
1345 first_event_at: chrono::DateTime<chrono::Utc>,
1346 checksum_matched: bool,
1347 ) -> Self {
1348 let semver = body.semver();
1349 let time =
1350 first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
1351
1352 Self {
1353 app_version: body.app_version.clone(),
1354 major: semver.map(|v| v.major() as i32),
1355 minor: semver.map(|v| v.minor() as i32),
1356 patch: semver.map(|v| v.patch() as i32),
1357 checksum_matched,
1358 release_channel: body.release_channel.clone().unwrap_or_default(),
1359 os_name: body.os_name.clone(),
1360 os_version: body.os_version.clone().unwrap_or_default(),
1361 installation_id: body.installation_id.clone(),
1362 session_id: body.session_id.clone(),
1363 is_staff: body.is_staff,
1364 time: time.timestamp_millis(),
1365 source: event.source,
1366 action: event.action,
1367 }
1368 }
1369}
1370
1371pub fn calculate_json_checksum(app: Arc<AppState>, json: &impl AsRef<[u8]>) -> Option<Vec<u8>> {
1372 let checksum_seed = app.config.zed_client_checksum_seed.as_ref()?;
1373
1374 let mut summer = Sha256::new();
1375 summer.update(checksum_seed);
1376 summer.update(json);
1377 summer.update(checksum_seed);
1378 Some(summer.finalize().into_iter().collect())
1379}
1380
1381fn for_snowflake(
1382 body: EventRequestBody,
1383 first_event_at: chrono::DateTime<chrono::Utc>,
1384 country_code: Option<String>,
1385) -> impl Iterator<Item = SnowflakeRow> {
1386 body.events.into_iter().flat_map(move |event| {
1387 let timestamp =
1388 first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
1389 let (event_type, mut event_properties) = match &event.event {
1390 Event::Editor(e) => (
1391 match e.operation.as_str() {
1392 "open" => "Editor Opened".to_string(),
1393 "save" => "Editor Saved".to_string(),
1394 _ => format!("Unknown Editor Event: {}", e.operation),
1395 },
1396 serde_json::to_value(e).unwrap(),
1397 ),
1398 Event::InlineCompletion(e) => (
1399 format!(
1400 "Inline Completion {}",
1401 if e.suggestion_accepted {
1402 "Accepted"
1403 } else {
1404 "Discarded"
1405 }
1406 ),
1407 serde_json::to_value(e).unwrap(),
1408 ),
1409 Event::Call(e) => {
1410 let event_type = match e.operation.trim() {
1411 "unshare project" => "Project Unshared".to_string(),
1412 "open channel notes" => "Channel Notes Opened".to_string(),
1413 "share project" => "Project Shared".to_string(),
1414 "join channel" => "Channel Joined".to_string(),
1415 "hang up" => "Call Ended".to_string(),
1416 "accept incoming" => "Incoming Call Accepted".to_string(),
1417 "invite" => "Participant Invited".to_string(),
1418 "disable microphone" => "Microphone Disabled".to_string(),
1419 "enable microphone" => "Microphone Enabled".to_string(),
1420 "enable screen share" => "Screen Share Enabled".to_string(),
1421 "disable screen share" => "Screen Share Disabled".to_string(),
1422 "decline incoming" => "Incoming Call Declined".to_string(),
1423 "enable camera" => "Camera Enabled".to_string(),
1424 "disable camera" => "Camera Disabled".to_string(),
1425 _ => format!("Unknown Call Event: {}", e.operation),
1426 };
1427
1428 (event_type, serde_json::to_value(e).unwrap())
1429 }
1430 Event::Assistant(e) => (
1431 match e.phase {
1432 telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(),
1433 telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
1434 telemetry_events::AssistantPhase::Accepted => {
1435 "Assistant Response Accepted".to_string()
1436 }
1437 telemetry_events::AssistantPhase::Rejected => {
1438 "Assistant Response Rejected".to_string()
1439 }
1440 },
1441 serde_json::to_value(e).unwrap(),
1442 ),
1443 Event::Cpu(_) | Event::Memory(_) => return None,
1444 Event::App(e) => {
1445 let mut properties = json!({});
1446 let event_type = match e.operation.trim() {
1447 "extensions: install extension" => "Extension Installed".to_string(),
1448 "open" => "App Opened".to_string(),
1449 "project search: open" => "Project Search Opened".to_string(),
1450 "first open" => {
1451 properties["is_first_open"] = json!(true);
1452 "App First Opened".to_string()
1453 }
1454 "extensions: uninstall extension" => "Extension Uninstalled".to_string(),
1455 "welcome page: close" => "Welcome Page Closed".to_string(),
1456 "open project" => {
1457 properties["is_first_time"] = json!(false);
1458 "Project Opened".to_string()
1459 }
1460 "welcome page: install cli" => "CLI Installed".to_string(),
1461 "project diagnostics: open" => "Project Diagnostics Opened".to_string(),
1462 "extensions page: open" => "Extensions Page Opened".to_string(),
1463 "welcome page: change theme" => "Welcome Theme Changed".to_string(),
1464 "welcome page: toggle metric telemetry" => {
1465 properties["enabled"] = json!(false);
1466 "Welcome Telemetry Toggled".to_string()
1467 }
1468 "welcome page: change keymap" => "Keymap Changed".to_string(),
1469 "welcome page: toggle vim" => {
1470 properties["enabled"] = json!(false);
1471 "Welcome Vim Mode Toggled".to_string()
1472 }
1473 "welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(),
1474 "welcome page: toggle diagnostic telemetry" => {
1475 "Welcome Telemetry Toggled".to_string()
1476 }
1477 "welcome page: open" => "Welcome Page Opened".to_string(),
1478 "close" => "App Closed".to_string(),
1479 "markdown preview: open" => "Markdown Preview Opened".to_string(),
1480 "welcome page: open extensions" => "Extensions Page Opened".to_string(),
1481 "open node project" | "open pnpm project" | "open yarn project" => {
1482 properties["project_type"] = json!("node");
1483 properties["is_first_time"] = json!(false);
1484 "Project Opened".to_string()
1485 }
1486 "repl sessions: open" => "REPL Session Started".to_string(),
1487 "welcome page: toggle helix" => {
1488 properties["enabled"] = json!(false);
1489 "Helix Mode Toggled".to_string()
1490 }
1491 "welcome page: edit settings" => {
1492 properties["changed_settings"] = json!([]);
1493 "Settings Edited".to_string()
1494 }
1495 "welcome page: view docs" => "Documentation Viewed".to_string(),
1496 "open ssh project" => {
1497 properties["is_first_time"] = json!(false);
1498 "SSH Project Opened".to_string()
1499 }
1500 "create ssh server" => "SSH Server Created".to_string(),
1501 "create ssh project" => "SSH Project Created".to_string(),
1502 "first open for release channel" => {
1503 properties["is_first_for_channel"] = json!(true);
1504 "App First Opened For Release Channel".to_string()
1505 }
1506 "feature upsell: toggle vim" => {
1507 properties["source"] = json!("Feature Upsell");
1508 "Vim Mode Toggled".to_string()
1509 }
1510 _ => e
1511 .operation
1512 .strip_prefix("feature upsell: viewed docs (")
1513 .and_then(|s| s.strip_suffix(')'))
1514 .map_or_else(
1515 || format!("Unknown App Event: {}", e.operation),
1516 |docs_url| {
1517 properties["url"] = json!(docs_url);
1518 properties["source"] = json!("Feature Upsell");
1519 "Documentation Viewed".to_string()
1520 },
1521 ),
1522 };
1523 (event_type, properties)
1524 }
1525 Event::Setting(e) => (
1526 "Settings Changed".to_string(),
1527 serde_json::to_value(e).unwrap(),
1528 ),
1529 Event::Extension(e) => (
1530 "Extension Loaded".to_string(),
1531 serde_json::to_value(e).unwrap(),
1532 ),
1533 Event::Edit(e) => (
1534 "Editor Edited".to_string(),
1535 serde_json::to_value(e).unwrap(),
1536 ),
1537 Event::Action(e) => (
1538 "Action Invoked".to_string(),
1539 serde_json::to_value(e).unwrap(),
1540 ),
1541 Event::Repl(e) => (
1542 "Kernel Status Changed".to_string(),
1543 serde_json::to_value(e).unwrap(),
1544 ),
1545 };
1546
1547 if let serde_json::Value::Object(ref mut map) = event_properties {
1548 map.insert("app_version".to_string(), body.app_version.clone().into());
1549 map.insert("os_name".to_string(), body.os_name.clone().into());
1550 map.insert("os_version".to_string(), body.os_version.clone().into());
1551 map.insert("architecture".to_string(), body.architecture.clone().into());
1552 map.insert(
1553 "release_channel".to_string(),
1554 body.release_channel.clone().into(),
1555 );
1556 map.insert("signed_in".to_string(), event.signed_in.into());
1557 if let Some(country_code) = country_code.as_ref() {
1558 map.insert("country_code".to_string(), country_code.clone().into());
1559 }
1560 }
1561
1562 let user_properties = Some(serde_json::json!({
1563 "is_staff": body.is_staff,
1564 "Country": country_code.clone(),
1565 "OS": format!("{} {}", body.os_name, body.os_version.clone().unwrap_or_default()),
1566 "Version": body.app_version.clone(),
1567 }));
1568
1569 Some(SnowflakeRow {
1570 time: timestamp,
1571 user_id: body.metrics_id.clone(),
1572 device_id: body.system_id.clone(),
1573 event_type,
1574 event_properties,
1575 user_properties,
1576 insert_id: Some(Uuid::new_v4().to_string()),
1577 })
1578 })
1579}
1580
1581#[derive(Serialize, Deserialize)]
1582struct SnowflakeRow {
1583 pub time: chrono::DateTime<chrono::Utc>,
1584 pub user_id: Option<String>,
1585 pub device_id: Option<String>,
1586 pub event_type: String,
1587 pub event_properties: serde_json::Value,
1588 pub user_properties: Option<serde_json::Value>,
1589 pub insert_id: Option<String>,
1590}
1591
1592#[derive(Serialize, Deserialize)]
1593struct SnowflakeData {
1594 /// Identifier unique to each Zed installation (differs for stable, preview, dev)
1595 pub installation_id: Option<String>,
1596 /// Identifier unique to each logged in Zed user (randomly generated on first sign in)
1597 /// Identifier unique to each Zed session (differs for each time you open Zed)
1598 pub session_id: Option<String>,
1599 pub metrics_id: Option<String>,
1600 /// True for Zed staff, otherwise false
1601 pub is_staff: Option<bool>,
1602 /// Zed version number
1603 pub app_version: String,
1604 pub os_name: String,
1605 pub os_version: Option<String>,
1606 pub architecture: String,
1607 /// Zed release channel (stable, preview, dev)
1608 pub release_channel: Option<String>,
1609 pub signed_in: bool,
1610
1611 #[serde(flatten)]
1612 pub editor_event: Option<EditorEvent>,
1613 #[serde(flatten)]
1614 pub inline_completion_event: Option<InlineCompletionEvent>,
1615 #[serde(flatten)]
1616 pub call_event: Option<CallEvent>,
1617 #[serde(flatten)]
1618 pub assistant_event: Option<AssistantEvent>,
1619 #[serde(flatten)]
1620 pub cpu_event: Option<CpuEvent>,
1621 #[serde(flatten)]
1622 pub memory_event: Option<MemoryEvent>,
1623 #[serde(flatten)]
1624 pub app_event: Option<AppEvent>,
1625 #[serde(flatten)]
1626 pub setting_event: Option<SettingEvent>,
1627 #[serde(flatten)]
1628 pub extension_event: Option<ExtensionEvent>,
1629 #[serde(flatten)]
1630 pub edit_event: Option<EditEvent>,
1631 #[serde(flatten)]
1632 pub repl_event: Option<ReplEvent>,
1633 #[serde(flatten)]
1634 pub action_event: Option<ActionEvent>,
1635}